Compare commits
76 Commits
14960d5e87
...
v1.6.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc44b99d3 | ||
|
|
60afeb4cfd | ||
|
|
02ff8b1a96 | ||
|
|
2156df22c6 | ||
|
|
cd2a3fac55 | ||
|
|
6300a3588a | ||
|
|
45213103e4 | ||
|
|
91b8b424d6 | ||
|
|
0d1c9277e5 | ||
|
|
db16d26103 | ||
|
|
0eb64d0975 | ||
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c | ||
|
|
508066d39f | ||
|
|
70956c204e | ||
|
|
16a7eac0c6 | ||
|
|
37ac08b182 | ||
|
|
5ef80b362e | ||
|
|
78f19daf76 | ||
|
|
6caa4a61df | ||
|
|
bf55034b2e | ||
|
|
ba1114e78b | ||
|
|
5ccc3b30f0 | ||
| 8d83076be0 | |||
|
|
997a3ae822 | ||
|
|
034c193e4b | ||
|
|
4acc8d1c01 | ||
|
|
49ff15f18d | ||
|
|
7a02617d48 | ||
|
|
e52eef0491 | ||
|
|
a5118305d3 | ||
|
|
b51671b1d4 | ||
|
|
1643dcf8c2 | ||
|
|
17ab4cdd16 | ||
|
|
d9182131d9 | ||
|
|
26a7fe64be | ||
|
|
4669dec359 | ||
|
|
3f05fe753e | ||
|
|
a502acd234 | ||
|
|
69b199b6dc | ||
|
|
d5f6749f9e | ||
| ad7918c993 | |||
| 86447000b1 | |||
| 7da5eb917a | |||
| d65eb9ff0f | |||
| 895df7415b | |||
| 9abe9fea7f | |||
| ea45ce9d0a | |||
| 2c3fbb093a | |||
| 3c5fb83673 | |||
| e1dc8850c0 | |||
| 59622580a9 | |||
| bdd1837247 | |||
| 40b4b90ed8 | |||
| d4bdb76fda | |||
| f7fc1bdee2 | |||
| 1a751927fa | |||
| 987aa5c15f | |||
| d2a1cd0cc4 | |||
| 5222a6bbf9 | |||
| 15e0b23f15 | |||
| fab1d25871 | |||
| 037ed782a7 | |||
| de8b05a553 | |||
| 6f9e1ec626 | |||
| 8430e9baef | |||
| fca3104a39 | |||
| 50336694f6 | |||
| c99f76d755 |
6
.env
6
.env
@@ -39,3 +39,9 @@ DEFAULT_URI=http://localhost
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=281e2cd303ed9ba4a4a4074e19eac9cea505cc9d82ce79a448bb8eb00c636ebe
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -23,3 +23,18 @@
|
||||
###> docker ###
|
||||
docker/.env.docker.local
|
||||
###< docker ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
###> migration archives ###
|
||||
/_archives/
|
||||
###< migration archives ###
|
||||
|
||||
###> frontend ###
|
||||
/frontend/node_modules/
|
||||
/frontend/.nuxt/
|
||||
/frontend/.output/
|
||||
/frontend/dist/
|
||||
###< frontend ###
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "Inventory_frontend"]
|
||||
path = Inventory_frontend
|
||||
url = gitea@gitea.malio.fr:MALIO-DEV/Inventory_frontend.git
|
||||
0
.idea/ferme.iml → .idea/Inventory.iml
generated
0
.idea/ferme.iml → .idea/Inventory.iml
generated
425
CARNET_DE_BORD.md
Normal file
425
CARNET_DE_BORD.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# 📔 Carnet de Bord - Migration Inventory → Symfony
|
||||
|
||||
**Projet** : Migration backend NestJS/Prisma → Symfony/API Platform
|
||||
**Début** : 2026-01-10
|
||||
**Objectif** : Migrer vers Symfony + JWT + API Platform propre et maintenable
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Convention de liaison des commits (INV)
|
||||
|
||||
- **Format** : `[INV-YYYYMMDD-XX]`
|
||||
- **Usage** : même code dans les commits du backend **et** du frontend + ajout ici pour retrouver le duo rapidement.
|
||||
|
||||
## 🧾 Journal des liaisons INV
|
||||
|
||||
- INV-20260111-01 : ajout du lien submodule `Inventory_frontend` (commit backend : `987aa5c`, commit frontend : `936a73f`)
|
||||
- INV-20260111-02 : alignement front API Platform + sessions (commit backend : `f7fc1bd`, commit frontend : `e99f053`)
|
||||
|
||||
## 🎯 Contexte
|
||||
|
||||
- **Situation initiale** :
|
||||
- `Inventory_backend/` : NestJS + Prisma (fonctionnel, ~11k lignes)
|
||||
- `Inventory_frontend/` : Nuxt 3 (fonctionnel, 105 fichiers)
|
||||
- Base de données PostgreSQL avec données en production
|
||||
|
||||
- **Objectif** :
|
||||
- Backend Symfony 8 + API Platform + JWT
|
||||
- Garder les données existantes (migration Prisma → Doctrine)
|
||||
- Frontend Nuxt connecté au nouveau backend
|
||||
- Docker : 2 backends en parallèle pendant transition
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 1 : Préparation (TERMINÉE - 10/01/2026)
|
||||
|
||||
### Ce qui a été fait
|
||||
|
||||
#### 1. Docker & Infrastructure ✅
|
||||
- **pgAdmin ajouté** au docker-compose.yml
|
||||
- Port : 5050
|
||||
- Login : admin@admin.com / admin
|
||||
- Container : `pgadmin-inventory`
|
||||
- Volume persistant : `pgadmin_data`
|
||||
- **Serveur PostgreSQL pré-configuré** :
|
||||
- Fichier `docker/pgadmin/servers.json` monté automatiquement
|
||||
- Fichier `docker/pgadmin/pgpass` pour authentification sans mot de passe
|
||||
- Connexion automatique à `db:5432/inventory` au démarrage
|
||||
- Nom du serveur : "Inventory PostgreSQL"
|
||||
|
||||
#### 2. Bundles Symfony installés ✅
|
||||
```bash
|
||||
# Versions installées
|
||||
- lexik/jwt-authentication-bundle: v3.2.0
|
||||
- vich/uploader-bundle: v2.9.1
|
||||
- symfony/uid: 8.0.*
|
||||
```
|
||||
|
||||
#### 3. JWT Configuration ✅
|
||||
- **Clés RSA générées** : `config/jwt/private.pem` + `public.pem`
|
||||
- **security.yaml configuré** :
|
||||
- Firewall `login` : pattern `^/api/login_check` avec `json_login`
|
||||
- Firewall `api` : pattern `^/api` avec `jwt` authenticator
|
||||
- Provider : `app_user_provider` (entité Profile via email)
|
||||
- Password hasher : bcrypt auto
|
||||
|
||||
#### 4. Entité Profile créée ✅
|
||||
**Fichier** : `src/Entity/Profile.php`
|
||||
|
||||
**Caractéristiques** :
|
||||
- Implémente `UserInterface` + `PasswordAuthenticatedUserInterface`
|
||||
- Champs :
|
||||
- `id` : string (30 chars, CUID-compatible pour Prisma)
|
||||
- `email` : string unique (username pour JWT)
|
||||
- `password` : string (hashed)
|
||||
- `roles` : array JSON (ROLE_USER par défaut)
|
||||
- `firstName`, `lastName` : string
|
||||
- `isActive` : boolean
|
||||
- `createdAt`, `updatedAt` : DateTimeImmutable
|
||||
- Repository : `ProfileRepository` avec `PasswordUpgraderInterface`
|
||||
- API Platform : endpoints CRUD auto-générés
|
||||
|
||||
#### 5. Base de Données ✅
|
||||
- **Migration créée** : `Version20260110175413`
|
||||
- **Table** : `profiles` créée avec succès
|
||||
- **Utilisateur test créé** :
|
||||
```
|
||||
Email: admin@admin.com
|
||||
Password: admin123
|
||||
Roles: ['ROLE_USER', 'ROLE_ADMIN']
|
||||
```
|
||||
|
||||
#### 6. API Platform ✅
|
||||
- **Endpoint racine** : http://localhost:8081/api/
|
||||
- **Réponse** :
|
||||
```json
|
||||
{
|
||||
"@context": "/api/contexts/Entrypoint",
|
||||
"@id": "/api/",
|
||||
"@type": "Entrypoint",
|
||||
"profile": "/api/profiles"
|
||||
}
|
||||
```
|
||||
- **OpenAPI Docs** : Configurées (à tester)
|
||||
|
||||
#### 7. Configuration Apache ✅
|
||||
- **VirtualHost** : `docker/php/config/vhost.conf`
|
||||
- **DocumentRoot** : `/var/www/html/public`
|
||||
- **AllowOverride** : All (pour `.htaccess`)
|
||||
- **Port** : 8081 (Apache) → accessible depuis l'hôte
|
||||
|
||||
#### 8. Routing Symfony ✅
|
||||
- **Routes définies** :
|
||||
- `/api/login_check` : Login JWT
|
||||
- `/api/test` : Test endpoint (TestController)
|
||||
- `/api/*` : API Platform auto-routes
|
||||
- **Vérification** :
|
||||
```bash
|
||||
php bin/console debug:router api_test
|
||||
php bin/console router:match /api/test --method=GET
|
||||
# ✅ Route found and matches
|
||||
```
|
||||
|
||||
#### 9. .htaccess créé ✅
|
||||
**Fichier** : `public/.htaccess`
|
||||
|
||||
**Contenu** : Symfony standard avec mod_rewrite
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ Problèmes identifiés
|
||||
|
||||
#### 1. Routes inaccessibles via Apache (404)
|
||||
|
||||
**Symptôme** :
|
||||
```bash
|
||||
curl http://localhost:8081/api/test
|
||||
# → 404 Not Found
|
||||
```
|
||||
|
||||
**Tests effectués** :
|
||||
- ✅ Route existe : `php bin/console debug:router api_test`
|
||||
- ✅ Route match : `php bin/console router:match /api/test`
|
||||
- ✅ Symfony fonctionne : `curl http://localhost:8081/api/` → JSON OK
|
||||
- ✅ PHP built-in server OK :
|
||||
```bash
|
||||
php -S localhost:9000
|
||||
curl http://localhost:9000/api/test
|
||||
# → {"status":"ok","message":"Test endpoint works!"}
|
||||
```
|
||||
- ❌ Apache 404 : Depuis l'hôte via port 8081
|
||||
|
||||
**Diagnostic** :
|
||||
- Le problème est **Apache-spécifique**
|
||||
- Symfony/PHP fonctionnent correctement
|
||||
- Le `.htaccess` n'est probablement **PAS lu par Apache**
|
||||
- Hypothèses :
|
||||
1. `AllowOverride All` non pris en compte
|
||||
2. `mod_rewrite` mal configuré
|
||||
3. Ordre des directives Apache incorrect
|
||||
4. Problème de permissions sur `.htaccess`
|
||||
|
||||
**Actions à faire** :
|
||||
- [ ] Vérifier permissions `.htaccess` dans container
|
||||
- [ ] Tester `apache2ctl configtest`
|
||||
- [ ] Activer logs de rewrite : `LogLevel alert rewrite:trace3`
|
||||
- [ ] Tester FallbackResource dans vhost au lieu de `.htaccess`
|
||||
|
||||
#### 2. JWT Login non testé
|
||||
|
||||
**Raison** : Bloqué par problème #1 (routes inaccessibles)
|
||||
|
||||
**Actions à faire** :
|
||||
- [ ] Résoudre problème Apache
|
||||
- [ ] Tester `POST /api/login_check` avec credentials
|
||||
- [ ] Vérifier génération du token JWT
|
||||
- [ ] Tester route protégée avec token
|
||||
|
||||
---
|
||||
|
||||
## 📝 Configuration Actuelle
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "8081:80" # Symfony API
|
||||
- "3001:3000" # (prévu pour Nuxt)
|
||||
|
||||
db:
|
||||
ports:
|
||||
- "5433:5432" # PostgreSQL
|
||||
|
||||
pgadmin:
|
||||
ports:
|
||||
- "5050:80" # pgAdmin
|
||||
```
|
||||
|
||||
### Variables d'Environnement
|
||||
|
||||
**Fichier** : `docker/.env.docker.local`
|
||||
|
||||
```env
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=inventory
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5433
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 2 : Migration DB + Frontend (TERMINÉE - 10/01/2026)
|
||||
|
||||
### Ce qui a été fait
|
||||
|
||||
#### 1. Entités Doctrine alignées Prisma ✅
|
||||
- **Toutes les entités manquantes** créées (Machine, ModelType, Composant, Piece, Product, Links, Requirements, CustomField, Document, etc.)
|
||||
- **IDs en string(36)** pour compatibilité CUID/UUID
|
||||
- **Colonnes Prisma en camelCase** conservées via `name="..."` (ex: `machineId`, `createdAt`, `supplierPrice`)
|
||||
- **Corrections** :
|
||||
- `Document.path` passé en `TEXT`
|
||||
- `CustomField.options` nullable
|
||||
- `TypeMachineComponentRequirement.required` corrigé
|
||||
|
||||
#### 2. Migration DB inventory-data → inventory ✅
|
||||
- **Dump data-only + normalisation** (conversion des identifiants quoted vers lowercase)
|
||||
- **Mapping table Prisma** : `"ModelType"` → `model_types`
|
||||
- **Exclusions** : `profiles`, `_prisma_migrations`
|
||||
- **Import validé** : `Counts match for all tables.`
|
||||
|
||||
Scripts utiles :
|
||||
```bash
|
||||
scripts/normalize-dump.py
|
||||
scripts/validate-migration.php
|
||||
```
|
||||
|
||||
#### 3. Frontend basculé sur Inventory_frontend ✅
|
||||
- `make dev-nuxt` pointe vers `Inventory_frontend/`
|
||||
- `README.md` mis à jour
|
||||
- **Base API** ajustée : `http://localhost:8081/api`
|
||||
|
||||
Fichiers modifiés :
|
||||
```
|
||||
makefile
|
||||
README.md
|
||||
Inventory_frontend/.env
|
||||
Inventory_frontend/nuxt.config.ts
|
||||
Inventory_frontend/app/services/modelTypes.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# pgAdmin
|
||||
PGADMIN_EMAIL=admin@admin.com
|
||||
PGADMIN_PASSWORD=admin
|
||||
PGADMIN_PORT=5050
|
||||
|
||||
# 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_change_me
|
||||
|
||||
# NestJS (pour futur parallèle)
|
||||
NESTJS_PORT=3000
|
||||
SESSION_SECRET=changeme_session_secret
|
||||
CORS_ORIGIN=http://localhost:3001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Phase 2 : Debugging & Tests (EN COURS)
|
||||
|
||||
### Objectifs
|
||||
- [x] Résoudre problème Apache `.htaccess`
|
||||
- [ ] Tester authentification JWT complète
|
||||
- [ ] Créer endpoint de test public fonctionnel
|
||||
- [ ] Documenter la solution Apache
|
||||
|
||||
### Prochaines étapes
|
||||
1. **Fix Apache** : Logs de debug + test FallbackResource
|
||||
2. **Test JWT** : Login + génération token + route protégée
|
||||
3. **Documentation** : Documenter la config Apache qui fonctionne
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métriques
|
||||
|
||||
### Temps passé
|
||||
- **Phase 1** : ~3h (exploration + setup + debugging)
|
||||
- **Problème Apache** : ~1h30 (en cours)
|
||||
|
||||
### Fichiers créés/modifiés
|
||||
|
||||
**Nouveaux fichiers** :
|
||||
- `src/Entity/Profile.php`
|
||||
- `src/Repository/ProfileRepository.php`
|
||||
- `src/Controller/TestController.php`
|
||||
- `public/.htaccess`
|
||||
- `config/routes/routing.controllers.yaml`
|
||||
- `create_test_user.php` (script utilitaire)
|
||||
- `migrations/Version20260110175413.php`
|
||||
- `docker/pgadmin/servers.json` (config serveur PostgreSQL)
|
||||
- `docker/pgadmin/pgpass` (credentials PostgreSQL)
|
||||
- `CARNET_DE_BORD.md` (ce fichier)
|
||||
|
||||
**Fichiers modifiés** :
|
||||
- `docker-compose.yml` (+ pgAdmin)
|
||||
- `docker/.env.docker.local` (+ variables Symfony/JWT/pgAdmin)
|
||||
- `docker/php/config/vhost.conf` (DocumentRoot → public/)
|
||||
- `config/packages/security.yaml` (JWT firewalls)
|
||||
- `config/routes.yaml` (+ api_login_check)
|
||||
- `composer.json` (+ lexik JWT, vich uploader)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Leçons Apprises
|
||||
|
||||
### 1. Symfony 8 + API Platform
|
||||
- **Attributs PHP 8** : `use Symfony\Component\Routing\Attribute\Route;` (pas `Annotation`)
|
||||
- **Routes controllers** : Nécessite `config/routes/routing.controllers.yaml` avec `type: attribute`
|
||||
- **API Platform** : Auto-génère les endpoints CRUD avec `#[ApiResource]`
|
||||
|
||||
### 2. JWT Authentication
|
||||
- **3 composants** :
|
||||
1. Firewall `login` : `json_login` intercepte `/api/login_check`
|
||||
2. Firewall `api` : `jwt` vérifie le token sur `/api/*`
|
||||
3. Access control : `PUBLIC_ACCESS` vs `IS_AUTHENTICATED_FULLY`
|
||||
- **username_path** : Permet de mapper `email` au lieu de `username`
|
||||
- **Provider** : Doit être défini dans le firewall `login`
|
||||
|
||||
### 3. Doctrine Migrations
|
||||
- **ID Prisma CUID** : Garder en `string(30)` pour compatibilité
|
||||
- **Lifecycle callbacks** : `#[ORM\PrePersist]` pour `createdAt`/`updatedAt`
|
||||
- **UserInterface** : Nécessite `getUserIdentifier()`, `getRoles()`, `eraseCredentials()`
|
||||
|
||||
### 4. Docker & Apache
|
||||
- **`.htaccess` vs VirtualHost** : Le vhost peut override le `.htaccess`
|
||||
- **AllowOverride All** : Indispensable pour que `.htaccess` fonctionne
|
||||
- **FallbackResource** : Alternative au mod_rewrite dans `.htaccess`
|
||||
- **Debugging** : Tester avec PHP built-in server pour isoler le problème
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ressources Utiles
|
||||
|
||||
### Accès aux Services
|
||||
|
||||
```
|
||||
🌐 pgAdmin: http://localhost:5050
|
||||
└─ Login: admin@admin.com / admin
|
||||
└─ Serveur: "Inventory PostgreSQL" (pré-configuré)
|
||||
└─ Database: inventory
|
||||
└─ Note: Le serveur PostgreSQL est automatiquement connecté au démarrage
|
||||
|
||||
🌐 API Platform: http://localhost:8081/api/
|
||||
└─ Docs: http://localhost:8081/api/docs (à venir)
|
||||
|
||||
🗄️ PostgreSQL: localhost:5433
|
||||
└─ User: root / root
|
||||
└─ Database: inventory
|
||||
```
|
||||
|
||||
### Commandes fréquentes
|
||||
|
||||
```bash
|
||||
# Symfony
|
||||
make shell # Entrer dans le container
|
||||
php bin/console cache:clear # Clear cache
|
||||
make cache-clear-full # Clear cache + purge var/cache
|
||||
php bin/console debug:router # Lister routes
|
||||
php bin/console debug:firewall # Lister firewalls
|
||||
php bin/console doctrine:migrations:migrate # Exécuter migrations
|
||||
|
||||
# Docker
|
||||
make start # Démarrer containers
|
||||
make stop # Arrêter containers
|
||||
docker logs -f php-inventory-apache # Logs Apache
|
||||
docker logs -f pgadmin-inventory # Logs pgAdmin
|
||||
docker exec php-inventory-apache bash # Shell root
|
||||
|
||||
# Tests API
|
||||
curl http://localhost:8081/api/ # Test API Platform
|
||||
curl -X POST http://localhost:8081/api/login_check \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@admin.com","password":"admin123"}'
|
||||
```
|
||||
|
||||
### Documentation
|
||||
- [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst)
|
||||
- [API Platform Security](https://api-platform.com/docs/core/security/)
|
||||
- [Symfony Security](https://symfony.com/doc/current/security.html)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Historique des Changements
|
||||
|
||||
### 2026-01-15 - Session 3
|
||||
- ✅ Filtre API Platform `category` sur `ModelType`
|
||||
- ✅ Normalisation des structures `ModelType` (structure ↔ skeleton)
|
||||
- ✅ Migration `custom_fields.options` en JSON
|
||||
- ✅ Ajout commande `make cache-clear-full`
|
||||
- ✅ Correctifs frontend: headers API Platform, pagination par catégorie, persistance tri
|
||||
|
||||
### 2026-01-10 - Session 2 (20h30)
|
||||
- ✅ Problème Apache résolu (routes fonctionnelles)
|
||||
- ✅ Phase 2 complète (JWT 100% opérationnel)
|
||||
- ✅ Authentification testée avec succès
|
||||
- ✅ Réorganisation projet (frontend/ + _archives/)
|
||||
- ✅ État des lieux dans MIGRATION_PLAN.md
|
||||
- ✅ 5 commits conventionnels créés
|
||||
- 📊 Base inventory-data analysée (673 lignes)
|
||||
|
||||
### 2026-01-10 - Session 1 (19h00)
|
||||
- ✅ Création projet migration
|
||||
- ✅ Phase 1 complète (pgAdmin, JWT, Profile, migrations)
|
||||
- ⚠️ Problème Apache identifié (routes 404)
|
||||
- 📝 Carnet de bord créé
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-01-15 13:45
|
||||
**Statut** : Phase 3 EN COURS ⚠️ - Migrations et intégration frontend
|
||||
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
Liste des évolutions du projet Ferme
|
||||
Liste des évolutions du projet inventory
|
||||
|
||||
## [0.0.0]
|
||||
### Parameters
|
||||
|
||||
328
DEPLOY.md
Normal file
328
DEPLOY.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Inventory - Guide de Déploiement & Release
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
inventory.malio-dev.fr/ → Frontend Nuxt (statique)
|
||||
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
|
||||
```
|
||||
|
||||
| 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` |
|
||||
|
||||
---
|
||||
|
||||
## Prérequis serveur
|
||||
|
||||
- **OS** : Ubuntu/Debian
|
||||
- **PHP** : 8.4 avec extensions : pgsql, intl, zip, gd, mbstring, curl
|
||||
- **Node.js** : 20+
|
||||
- **Nginx**
|
||||
- **PostgreSQL** : 16
|
||||
- **Composer**
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
php -v # PHP 8.4+
|
||||
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
|
||||
node -v # Node 20+
|
||||
nginx -v
|
||||
psql --version
|
||||
composer --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Déploiement initial
|
||||
|
||||
### 1. Cloner le projet
|
||||
|
||||
```bash
|
||||
cd /var/www
|
||||
sudo git clone --recurse-submodules gitea@gitea.malio.fr:MALIO-DEV/Inventory.git Inventory
|
||||
sudo chown -R malio:malio Inventory
|
||||
cd Inventory
|
||||
git checkout master
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### 2. Créer la base de données
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql
|
||||
|
||||
CREATE DATABASE inventory OWNER ferme_user;
|
||||
GRANT ALL PRIVILEGES ON DATABASE inventory TO ferme_user;
|
||||
\q
|
||||
```
|
||||
|
||||
Importer le dump :
|
||||
```bash
|
||||
# Copier le dump depuis le PC local
|
||||
scp backup_v1.0.0_clean.sql malio@192.168.0.159:/tmp/
|
||||
|
||||
# Importer
|
||||
psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql
|
||||
```
|
||||
|
||||
### 3. Configurer le backend Symfony
|
||||
|
||||
```bash
|
||||
cd /var/www/Inventory
|
||||
|
||||
# Installer les dépendances
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Créer .env.local
|
||||
cat > .env.local << 'EOF'
|
||||
APP_ENV=prod
|
||||
APP_DEBUG=0
|
||||
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
|
||||
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
|
||||
sudo chown -R www-data:www-data var/
|
||||
sudo chmod -R 775 var/
|
||||
|
||||
# Vider le cache
|
||||
php bin/console cache:clear --env=prod
|
||||
```
|
||||
|
||||
### 4. Configurer le frontend Nuxt
|
||||
|
||||
```bash
|
||||
cd /var/www/Inventory/Inventory_frontend
|
||||
|
||||
# Permissions
|
||||
sudo chown -R malio:malio .
|
||||
|
||||
# Installer les dépendances
|
||||
npm install
|
||||
|
||||
# Créer .env
|
||||
cat > .env << 'EOF'
|
||||
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
|
||||
EOF
|
||||
|
||||
# Générer le site statique
|
||||
npx nuxi generate
|
||||
```
|
||||
|
||||
### 5. Configurer Nginx
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/inventory
|
||||
```
|
||||
|
||||
Contenu :
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name inventory.malio-dev.fr;
|
||||
|
||||
# Gros fichiers (100MB max)
|
||||
client_max_body_size 100M;
|
||||
client_body_timeout 300s;
|
||||
send_timeout 300s;
|
||||
|
||||
access_log /var/log/nginx/inventory-access.log;
|
||||
error_log /var/log/nginx/inventory-error.log;
|
||||
|
||||
# Backend Symfony - /api
|
||||
location /api {
|
||||
root /var/www/Inventory/public;
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/Inventory/public/index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /var/www/Inventory/public;
|
||||
fastcgi_read_timeout 300s;
|
||||
internal;
|
||||
}
|
||||
|
||||
# Frontend statique
|
||||
location / {
|
||||
root /var/www/Inventory/Inventory_frontend/.output/public;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Activer :
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 6. Vérifier
|
||||
|
||||
```bash
|
||||
curl http://inventory.malio-dev.fr
|
||||
curl http://inventory.malio-dev.fr/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mises à jour
|
||||
|
||||
### Mettre à jour l'application
|
||||
|
||||
```bash
|
||||
cd /var/www/Inventory
|
||||
|
||||
# Pull les changements
|
||||
git pull
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Backend
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php bin/console cache:clear --env=prod
|
||||
sudo chown -R www-data:www-data var/
|
||||
|
||||
# Frontend
|
||||
cd Inventory_frontend
|
||||
npm install
|
||||
npx nuxi generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Versioning & Releases
|
||||
|
||||
### 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
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
# Frontend (submodule)
|
||||
cd Inventory_frontend && git push && git push --tags && cd ..
|
||||
|
||||
# Backend
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
# Logs Nginx
|
||||
tail -f /var/log/nginx/inventory-error.log
|
||||
|
||||
# Logs Symfony
|
||||
tail -f /var/www/Inventory/var/log/prod.log
|
||||
|
||||
# Vider le cache Symfony
|
||||
php /var/www/Inventory/bin/console cache:clear --env=prod
|
||||
|
||||
# Rebuild frontend
|
||||
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
|
||||
|
||||
# Status PHP-FPM
|
||||
systemctl status php8.4-fpm
|
||||
|
||||
# Reload Nginx
|
||||
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
|
||||
```
|
||||
1
Inventory_frontend
Submodule
1
Inventory_frontend
Submodule
Submodule Inventory_frontend added at 6bed715b7f
1419
MIGRATION_PLAN.md
Normal file
1419
MIGRATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
35
README.md
35
README.md
@@ -1,4 +1,4 @@
|
||||
# Projet Ferme
|
||||
# Projet Inventory
|
||||
|
||||
## Installation du projet
|
||||
### Windows
|
||||
@@ -12,6 +12,7 @@ Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnem
|
||||
### Installation du projet
|
||||
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
||||
```bash
|
||||
sudo apt install make -y
|
||||
make start
|
||||
make install
|
||||
```
|
||||
@@ -20,7 +21,7 @@ Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifi
|
||||
### Configuration xdebug
|
||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||
* Name : ferme-docker
|
||||
* Name : inventory-docker
|
||||
* Host : localhost
|
||||
* Port : 8080
|
||||
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||
@@ -36,12 +37,42 @@ Vous pouvez modifier le port si nécessaire.
|
||||
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
||||
C'est un bdd local dans le docker.
|
||||
### Frontend
|
||||
Le frontend utilise le dossier `Inventory_frontend/`.
|
||||
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||
```bash
|
||||
make dev-nuxt
|
||||
```
|
||||
Le front sera accessible sur http://localhost:3000
|
||||
|
||||
## Compression automatique des PDFs
|
||||
|
||||
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
|
||||
|
||||
### Prérequis
|
||||
```bash
|
||||
# Installation de qpdf (outil système)
|
||||
sudo apt install qpdf
|
||||
|
||||
# Ou dans Docker
|
||||
docker exec -it php-inventory-apache apt update && apt install -y qpdf
|
||||
```
|
||||
|
||||
### Fonctionnement
|
||||
- À chaque upload de PDF, le système compresse automatiquement le fichier
|
||||
- Compression lossless (sans perte de qualité)
|
||||
- Le PDF est compressé uniquement si la taille diminue
|
||||
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
|
||||
|
||||
### Compresser les PDFs existants
|
||||
Pour compresser tous les PDFs déjà en base :
|
||||
```bash
|
||||
# Voir ce qui serait compressé (dry-run)
|
||||
php bin/console app:compress-pdf --dry-run
|
||||
|
||||
# Compresser tous les PDFs
|
||||
php bin/console app:compress-pdf
|
||||
```
|
||||
|
||||
## Commandes utiles
|
||||
Pour restart le container
|
||||
```bash
|
||||
|
||||
786
REFACTORING_PLAN.md
Normal file
786
REFACTORING_PLAN.md
Normal file
@@ -0,0 +1,786 @@
|
||||
# Plan de Refactoring - Inventory v1.2.0
|
||||
|
||||
> **Date de creation :** 2026-02-03
|
||||
> **Branche de travail :** `refacto/v1.3.0`
|
||||
> **Base :** `develop` (commit `8d83076`)
|
||||
|
||||
---
|
||||
|
||||
## Legende des statuts
|
||||
|
||||
| Statut | Signification |
|
||||
| ------ | ---------------------- |
|
||||
| `[ ]` | A faire |
|
||||
| `[~]` | En cours |
|
||||
| `[x]` | Termine |
|
||||
| `[!]` | Bloque / besoin d'info |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Securite (CRITIQUE)
|
||||
|
||||
> **Priorite :** MAXIMALE - A traiter en premier
|
||||
|
||||
### 1.1 Corriger la configuration de securite
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `config/packages/security.yaml`
|
||||
- **Probleme :** `PUBLIC_ACCESS` applique a toutes les routes `/api` avant la regle `IS_AUTHENTICATED_FULLY`. Le pattern matching "first match wins" rend potentiellement tout public.
|
||||
- **Action :** Reordonner les regles `access_control` pour que les routes protegees soient listees AVANT les routes publiques.
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 1.2 Ajouter les controles d'autorisation sur les controllers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Controller/MachineSkeletonController.php`
|
||||
- `src/Controller/CustomFieldValueController.php`
|
||||
- `src/Controller/DocumentQueryController.php`
|
||||
- `src/Controller/SessionProfileController.php`
|
||||
- `src/Controller/SessionProfilesController.php`
|
||||
- Tous les `*HistoryController.php`
|
||||
- **Probleme :** Aucun attribut `#[IsGranted]` sur les controllers custom. Pas de RBAC.
|
||||
- **Action :** Ajouter `#[IsGranted('IS_AUTHENTICATED_FULLY')]` sur chaque controller (ou route). Definir des roles si necessaire.
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 1.3 Securiser les secrets
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `.env` (JWT_PASSPHRASE en dur, APP_SECRET vide)
|
||||
- `docker/.env.docker` (credentials `root:root`)
|
||||
- **Action :**
|
||||
1. Deplacer `JWT_PASSPHRASE` dans `.env.local` (git-ignore)
|
||||
2. Generer un `APP_SECRET` valide
|
||||
3. Ajouter `.env.local` dans `.gitignore` si pas deja fait
|
||||
4. Documenter la configuration des secrets pour les devs
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - Elimination de la duplication de code
|
||||
|
||||
> **Priorite :** HAUTE - Impact direct sur la maintenabilite
|
||||
|
||||
### 2.1 Refactorer les 3 Audit Subscribers en un seul generique
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers concernes :**
|
||||
- `src/EventSubscriber/ProductAuditSubscriber.php` (298 LOC)
|
||||
- `src/EventSubscriber/PieceAuditSubscriber.php` (300 LOC)
|
||||
- `src/EventSubscriber/ComposantAuditSubscriber.php` (300 LOC)
|
||||
- **Probleme :** ~900 LOC dupliquees a ~95%. Les methodes `onFlush()`, `buildDiffFromChangeSet()`, `resolveActorProfileId()`, `mergeDiffs()`, `normalizeCollection()` sont identiques. Seules les methodes `snapshot*()` different legerement.
|
||||
- **Action :**
|
||||
1. Creer un `AbstractAuditSubscriber` ou un `GenericAuditSubscriber` parametrable
|
||||
2. Extraire la logique commune (onFlush, buildDiff, resolveActor, mergeDiffs, normalizeCollection)
|
||||
3. Utiliser un systeme de configuration par entite (map `entityClass => entityType + snapshotMethod`)
|
||||
4. Supprimer les 3 fichiers redondants
|
||||
5. Verifier que l'audit fonctionne toujours sur Product, Piece et Composant
|
||||
- **Agent :** -
|
||||
- **Notes :** Tester manuellement les logs d'audit apres refacto.
|
||||
|
||||
### 2.2 Extraire un CuidGenerator utilitaire
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers concernes :** 18 entites contenant `generateCuid()` en prive
|
||||
- **Probleme :** Methode `generateCuid()` dupliquee dans chaque entite. De plus, `AuditLog.php` utilise une variante differente (base_convert).
|
||||
- **Action :**
|
||||
1. Creer `src/Util/CuidGenerator.php` avec une methode statique `generate(): string`
|
||||
2. Uniformiser l'implementation (choisir une seule methode)
|
||||
3. Remplacer tous les appels dans les 18 entites
|
||||
4. Supprimer les methodes privees devenues inutiles
|
||||
- **Agent :** -
|
||||
- **Notes :** Attention a l'inconsistance entre AuditLog et les autres entites.
|
||||
|
||||
### 2.3 Factoriser la logique de liaison dans MachineSkeletonController
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `src/Controller/MachineSkeletonController.php` (756 LOC)
|
||||
- **Probleme :** Les methodes `applyComponentLinks()`, `applyPieceLinks()`, `applyProductLinks()` sont quasi identiques (~90 LOC chacune).
|
||||
- **Action :**
|
||||
1. Extraire une methode generique `applyLinks(Machine $machine, array $links, string $type)`
|
||||
2. Parametrer par le type d'entite liee (Composant, Piece, Product)
|
||||
3. Reduire le controller a ~400 LOC max
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - Restructuration des controllers
|
||||
|
||||
> **Priorite :** MOYENNE - Amelioration de la lisibilite et maintenabilite
|
||||
|
||||
### 3.1 Decouper MachineSkeletonController
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `src/Controller/MachineSkeletonController.php` (756 LOC)
|
||||
- **Action :**
|
||||
1. Extraire la logique metier dans un `MachineSkeletonService`
|
||||
2. Le controller ne doit gerer que la requete/reponse HTTP
|
||||
3. Le service gere la logique de skeleton (get, update, applyLinks)
|
||||
4. Extraire les helpers (`resolveIdentifier`, `indexLinksById`, `applyOverrides`, `normalizeMachineSkeletonResponse`) dans le service
|
||||
- **Agent :** -
|
||||
- **Notes :** Depend de la phase 2.3 (factorisation des liens).
|
||||
|
||||
### 3.2 Ajouter un try-catch et du logging dans les controllers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :** Tous les controllers dans `src/Controller/`
|
||||
- **Probleme :** Aucun try-catch autour des `flush()` et `persist()`. Pas de logging d'erreurs.
|
||||
- **Action :**
|
||||
1. Ajouter `try-catch` autour des operations Doctrine dans chaque controller
|
||||
2. Logger les erreurs avec le `LoggerInterface` de Symfony (Monolog)
|
||||
3. Retourner des reponses JSON coherentes en cas d'erreur serveur (500)
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 3.3 Renforcer la validation des entrees
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Controller/CustomFieldValueController.php`
|
||||
- `src/Controller/MachineSkeletonController.php`
|
||||
- **Probleme :** Pas de validation de longueur max, pas de regex sur les IDs, pas de controle de profondeur JSON.
|
||||
- **Action :**
|
||||
1. Valider le format des IDs (regex CUID : `/^cl[a-f0-9]{24}$/`)
|
||||
2. Ajouter des limites de longueur sur les champs string
|
||||
3. Utiliser le composant Validator de Symfony pour les DTOs si pertinent
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 - Amelioration du stockage
|
||||
|
||||
> **Priorite :** MOYENNE - Performance et scalabilite
|
||||
|
||||
### 4.1 Migrer le stockage PDF de base64 vers le filesystem
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Entity/Document.php`
|
||||
- `src/Command/CompressPdfCommand.php`
|
||||
- `src/Service/PdfCompressorService.php`
|
||||
- **Probleme :** Les PDFs sont stockes en base64 dans la colonne `path` (TEXT) de la BDD. Risque de DoS et mauvaise perf sur des gros fichiers.
|
||||
- **Action :**
|
||||
1. Utiliser `vich/uploader-bundle` (deja installe) pour le stockage fichier
|
||||
2. Configurer un repertoire de stockage (`var/uploads/documents/`)
|
||||
3. Migrer les documents existants (script de migration)
|
||||
4. Adapter `PdfCompressorService` pour lire/ecrire sur le filesystem
|
||||
5. Mettre a jour l'entite Document
|
||||
- **Agent :** -
|
||||
- **Notes :** Prevoir une migration de donnees pour les documents existants.
|
||||
|
||||
### 4.2 Corriger les types de prix (string -> decimal)
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Entity/Machine.php` (`$prix`)
|
||||
- `src/Entity/Product.php` (`$supplierPrice`)
|
||||
- **Probleme :** Les prix sont types `?string` en PHP alors que la colonne est `DECIMAL(10,2)` en BDD.
|
||||
- **Action :**
|
||||
1. Changer le type PHP en `?float` ou utiliser `brick/money`
|
||||
2. Adapter les getters/setters
|
||||
3. Verifier la serialisation API Platform
|
||||
- **Agent :** -
|
||||
- **Notes :** Impact potentiel sur le frontend (format des nombres).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 - Utilisation du Process Component
|
||||
|
||||
> **Priorite :** BASSE - Bonne pratique
|
||||
|
||||
### 5.1 Remplacer exec() par Symfony Process
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :**
|
||||
- `src/Command/CompressPdfCommand.php` (lignes 42, 98-101)
|
||||
- `src/Service/PdfCompressorService.php` (lignes 37-41)
|
||||
- **Probleme :** Utilisation de `exec()` directe pour appeler `qpdf`.
|
||||
- **Action :**
|
||||
1. Remplacer par `Symfony\Component\Process\Process`
|
||||
2. Gerer le timeout et les erreurs proprement
|
||||
3. Tester que la compression fonctionne toujours
|
||||
- **Agent :** -
|
||||
- **Notes :** `escapeshellarg()` est deja utilise, donc pas de faille de securite immediate.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 - Tests
|
||||
|
||||
> **Priorite :** HAUTE - Indispensable avant toute refacto majeure
|
||||
|
||||
### 6.1 Mettre en place les tests unitaires
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Unit/Util/CuidGeneratorTest.php`
|
||||
- `tests/Unit/Entity/MachineTest.php`
|
||||
- `tests/Unit/Entity/ProductTest.php`
|
||||
- `tests/Unit/Service/PdfCompressorServiceTest.php`
|
||||
- **Action :**
|
||||
1. Tester le CuidGenerator (format, unicite)
|
||||
2. Tester les entites (validation, lifecycle callbacks)
|
||||
3. Tester le PdfCompressorService
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
### 6.2 Mettre en place les tests fonctionnels (API)
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Functional/Api/MachineTest.php`
|
||||
- `tests/Functional/Api/ProductTest.php`
|
||||
- `tests/Functional/Api/AuthenticationTest.php`
|
||||
- `tests/Functional/Api/MachineSkeletonTest.php`
|
||||
- **Action :**
|
||||
1. Configurer une base de test (SQLite ou PostgreSQL de test)
|
||||
2. Creer des fixtures de test
|
||||
3. Tester les endpoints CRUD
|
||||
4. Tester l'authentification JWT
|
||||
5. Tester les endpoints custom (skeleton, custom fields)
|
||||
- **Agent :** -
|
||||
- **Notes :** Utiliser `ApiTestCase` de API Platform.
|
||||
|
||||
### 6.3 Tests des Audit Subscribers
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/Unit/EventSubscriber/AuditSubscriberTest.php`
|
||||
- **Action :**
|
||||
1. Tester la creation de logs sur insert/update/delete
|
||||
2. Tester le format des diffs et snapshots
|
||||
3. Tester la resolution de l'acteur
|
||||
- **Agent :** -
|
||||
- **Notes :** A faire APRES la phase 2.1 (refacto des subscribers).
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 - Nett oyage et conventions
|
||||
|
||||
> **Priorite :** BASSE - Polish final
|
||||
|
||||
### 7.1 Supprimer les fichiers inutiles
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a verifier :**
|
||||
- `frontend/` (dossier legacy ? vs `Inventory_frontend/`)
|
||||
- `src/ApiResource/` (repertoire vide)
|
||||
- Fichiers SQL a la racine (`backup_v1.0.0.sql`, `data_norm.sql`, `fullasse.sql`, `fulldata.sql`)
|
||||
- **Action :** Confirmer avec l'equipe quels fichiers sont obsoletes et les supprimer.
|
||||
- **Agent :** -
|
||||
- **Notes :** Ne pas supprimer sans validation.
|
||||
|
||||
### 7.2 Uniformiser la gestion des null
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers :** Toutes les entites dans `src/Entity/`
|
||||
- **Action :** S'assurer que les types nullable sont coherents entre PHP et la BDD (colonnes NOT NULL vs nullable).
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# FRONTEND (`Inventory_frontend/`)
|
||||
|
||||
---
|
||||
|
||||
## Phase F1 - Decoupage des mega-composants (CRITIQUE)
|
||||
|
||||
> **Priorite :** MAXIMALE - Les fichiers actuels sont inmaintenables
|
||||
|
||||
### F1.1 Decouper `machine/[id].vue` (2989 LOC → 219 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/pages/machine/[id].vue`
|
||||
- **Resultat :** Page decomposee en 2 composables + 7 composants. Orchestrateur = 219 LOC.
|
||||
- **Fichiers crees :**
|
||||
- `composables/useMachineDetailData.ts` (1404 LOC) — state + logique metier
|
||||
- `composables/useMachineSkeletonEditor.ts` (843 LOC) — logique skeleton
|
||||
- `components/machine/MachineDetailHeader.vue` (76 LOC)
|
||||
- `components/machine/MachineInfoCard.vue` (185 LOC)
|
||||
- `components/machine/MachineDocumentsCard.vue` (116 LOC)
|
||||
- `components/machine/MachineProductsCard.vue` (62 LOC)
|
||||
- `components/machine/MachineComponentsCard.vue` (53 LOC)
|
||||
- `components/machine/MachinePiecesCard.vue` (34 LOC)
|
||||
- `components/machine/MachineSkeletonSummary.vue` (199 LOC)
|
||||
- **Pattern :** Props + Events (pas de provide/inject). Composables avec injection de dependances (interface Deps).
|
||||
- **Notes :** Typecheck 0 erreurs. Lint OK.
|
||||
|
||||
### F1.2 Decouper `machines/new.vue` (1231 LOC → 196 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/pages/machines/new.vue`
|
||||
- **Resultat :** Page decomposee en 1 composable + 5 composants. Orchestrateur = 196 LOC.
|
||||
- **Fichiers crees :**
|
||||
- `composables/useMachineCreatePage.ts` (460 LOC) — state, entity lookups, options, creation
|
||||
- `components/machine/create/RequirementComponentSelector.vue` (126 LOC)
|
||||
- `components/machine/create/RequirementPieceSelector.vue` (130 LOC)
|
||||
- `components/machine/create/RequirementProductSelector.vue` (142 LOC)
|
||||
- `components/machine/create/MachineCreatePreview.vue` (205 LOC)
|
||||
- `components/machine/create/PreviewRequirementGroup.vue` (59 LOC)
|
||||
- **Pattern :** Props + Events. Composable consolide entity lookups, options, label helpers, creation.
|
||||
- **Notes :** Typecheck 0 erreurs. Lint OK. Corrige aussi un bug F1.1 (defineProps dans mauvais script block de MachineSkeletonSummary.vue).
|
||||
|
||||
### F1.3 Decouper les pages de creation/edition (Piece, Component, Product)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers :**
|
||||
- `pages/component/create.vue` (1282 LOC)
|
||||
- `pages/component/[id]/edit.vue` (1629 LOC)
|
||||
- `pages/pieces/create.vue` (817 LOC)
|
||||
- `pages/pieces/[id]/edit.vue` (1327 LOC)
|
||||
- `pages/product/[id]/edit.vue` (936 LOC)
|
||||
- **Probleme :** Formulaires monolithiques avec sections multiples (infos generales, fournisseurs, documents, custom fields, etc.).
|
||||
- **Action :**
|
||||
1. Identifier les sections communes entre create/edit (factoriser)
|
||||
2. Extraire chaque section en composant reutilisable :
|
||||
- `EntityFormGeneral.vue` (nom, reference, description)
|
||||
- `EntityFormSuppliers.vue` (constructeurs)
|
||||
- `EntityFormDocuments.vue` (documents)
|
||||
- `EntityFormCustomFields.vue` (champs personnalises)
|
||||
3. Objectif par page : <400 LOC
|
||||
- **Agent :** -
|
||||
- **Notes :** Les formulaires create et edit partagent beaucoup de code. Factoriser.
|
||||
- **Sous-taches :**
|
||||
- [x] F1.3a Extraire `customFieldFormUtils.ts` (duplique dans 5 fichiers)
|
||||
- [x] F1.3b Extraire `documentDisplayUtils.ts` (duplique dans 3 pages edit)
|
||||
- [x] F1.3c Extraire `historyDisplayUtils.ts` (duplique dans 3 pages edit)
|
||||
- [x] F1.3d Rewire les 5 pages create/edit sur les modules extraits
|
||||
- [x] F1.3e Typecheck + commit F1.3 (erreurs F1.3 corrigees, 120 erreurs preexistantes documentees)
|
||||
|
||||
### F1.4 Reduire PieceItem.vue (1588 LOC) et ComponentItem.vue (1336 LOC)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers :**
|
||||
- `Inventory_frontend/app/components/PieceItem.vue` (1588 → 740 LOC)
|
||||
- `Inventory_frontend/app/components/ComponentItem.vue` (1336 → 585 LOC)
|
||||
- **Probleme :** ~700 LOC de logique dupliquee entre les deux composants (champs personnalises, documents, affichage produit).
|
||||
- **Action realisee :**
|
||||
1. Extraction de la logique pure custom fields dans `shared/utils/entityCustomFieldLogic.ts` (~350 LOC)
|
||||
2. Creation de `composables/useEntityCustomFields.ts` (composable reactif, ~180 LOC)
|
||||
3. Creation de `composables/useEntityDocuments.ts` (CRUD documents + preview, ~120 LOC)
|
||||
4. Creation de `composables/useEntityProductDisplay.ts` (affichage produit, ~100 LOC)
|
||||
5. Import des helpers document depuis `shared/utils/documentDisplayUtils.ts` (existant)
|
||||
6. Rewrite des deux composants pour utiliser les modules partages
|
||||
7. Typecheck 0 erreurs, lint 0 erreurs
|
||||
- **Sous-taches :**
|
||||
- [x] F1.4a Extraire `entityCustomFieldLogic.ts` (fonctions pures)
|
||||
- [x] F1.4b Creer `useEntityCustomFields.ts` (composable reactif)
|
||||
- [x] F1.4c Creer `useEntityDocuments.ts` (composable documents)
|
||||
- [x] F1.4d Creer `useEntityProductDisplay.ts` (composable produit)
|
||||
- [x] F1.4e Rewrite ComponentItem.vue (1336 → 585 LOC, script 900 → 150 LOC)
|
||||
- [x] F1.4f Rewrite PieceItem.vue (1588 → 740 LOC, script 1100 → 255 LOC)
|
||||
- [x] F1.4g Typecheck + lint (0 erreurs)
|
||||
- **Notes :** Les templates restent volumineux (~430-480 LOC) car le contenu UI est dense. Une extraction en sous-composants (DocumentList, ProductDisplay, CustomFieldForm) serait une etape future optionnelle.
|
||||
|
||||
---
|
||||
|
||||
## Phase F2 - Elimination de la duplication frontend
|
||||
|
||||
> **Priorite :** HAUTE - DRY
|
||||
|
||||
### F2.1 Extraire `extractCollection()` dans un utilitaire partage
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useSites.ts`
|
||||
- `composables/useProducts.ts`
|
||||
- `composables/usePieces.ts`
|
||||
- `composables/useComposants.ts`
|
||||
- `composables/useMachineTypesApi.js`
|
||||
- `composables/useConstructeurs.ts`
|
||||
- `composables/useDocuments.ts`
|
||||
- `composables/useMachineCreateSelections.ts`
|
||||
- `components/ComponentStructureAssignmentNode.vue`
|
||||
- `components/model-types/ManagementView.vue`
|
||||
- **Probleme :** La fonction `extractCollection()` (parsing `hydra:member` / `member` / `items` / `data` / array) etait dupliquee dans 10 fichiers.
|
||||
- **Action :**
|
||||
1. [x] Creer `shared/utils/apiHelpers.ts` avec `extractCollection<T>()` generique
|
||||
2. [x] Remplacer les 10 implementations locales par un import
|
||||
- **Agent :** -
|
||||
- **Notes :** Gere aussi `items` (utilise par ManagementView.vue). `extractRelationId()` et `normalizeRelationIds()` restent dans `shared/apiRelations.ts` (deja partages).
|
||||
|
||||
### F2.2 Fusionner les 3 composables d'historique
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useComponentHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/usePieceHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/useProductHistory.ts` (67 → 13 LOC, thin wrapper)
|
||||
- `composables/useEntityHistory.ts` (NEW, 65 LOC, logique generique)
|
||||
- **Probleme :** 3 fichiers quasi identiques (seul le endpoint differait).
|
||||
- **Action :**
|
||||
1. [x] Creer `composables/useEntityHistory.ts` parametrable par type d'entite
|
||||
2. [x] Reecrire les 3 fichiers specifiques en wrappers backward-compatible
|
||||
- **Agent :** -
|
||||
- **Notes :** Les wrappers preservent l'API existante (types + fonction), aucun consommateur a modifier.
|
||||
|
||||
### F2.3 Factoriser les composables de types (Component/Piece/Product)
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `composables/useComponentTypes.ts` (165 → 30 LOC, thin wrapper)
|
||||
- `composables/usePieceTypes.ts` (165 → 30 LOC, thin wrapper)
|
||||
- `composables/useProductTypes.ts` (160 → 28 LOC, thin wrapper)
|
||||
- `composables/useEntityTypes.ts` (NEW, 172 LOC, logique generique)
|
||||
- **Probleme :** 3 composables tres similaires pour gerer les categories/types.
|
||||
- **Action :**
|
||||
1. [x] Creer `composables/useEntityTypes.ts` generique (CRUD + singleton state par categorie)
|
||||
2. [x] Reecrire les 3 fichiers specifiques en wrappers avec renommage des champs
|
||||
- **Agent :** -
|
||||
- **Notes :** Les wrappers renomment `types` → `componentTypes`/`pieceTypes`/`productTypes`, preservent `getXxxTypes()` et `isXxxTypeLoading()`. Etat partage via `stateByCategory` map module-level.
|
||||
|
||||
---
|
||||
|
||||
## Phase F3 - Migration TypeScript
|
||||
|
||||
> **Priorite :** HAUTE - Securite du typage
|
||||
|
||||
### F3.1 Definir les types pour les reponses API
|
||||
|
||||
- **Statut :** `[x]` (partiellement — types definis dans chaque composable + `ApiResponse<T>` dans useApi.ts)
|
||||
- **Fichiers :**
|
||||
- `composables/useApi.ts` — `ApiResponse<T>` generique (success/data/error/status)
|
||||
- `composables/useMachines.ts` — `Machine` interface
|
||||
- `composables/useMachineTypesApi.ts` — `MachineType`, `MachineTypeRequirement` interfaces
|
||||
- `composables/useToast.ts` — `Toast`, `ToastType` types
|
||||
- `composables/useProfiles.ts` — `Profile` interface
|
||||
- `composables/useCustomFields.ts` — `CustomFieldValue` interface
|
||||
- **Notes :** Les types sont definis dans chaque composable (colocation). Types entite existants : `Product`, `Piece`, `Composant`, `Constructeur`, `Site`, `Document` dans leurs composables respectifs (.ts). `shared/types/inventory.ts` contient les types de structure de modele.
|
||||
|
||||
### F3.2 Convertir les composables JS en TS
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers convertis (7 fichiers JS → TS) :**
|
||||
- [x] `useToast.js` → `useToast.ts` (72 LOC, types: `Toast`, `ToastType`)
|
||||
- [x] `useProfiles.js` → `useProfiles.ts` (68 LOC, type: `Profile`)
|
||||
- [x] `useProfileSession.js` → `useProfileSession.ts` (85 LOC, importe `Profile`)
|
||||
- [x] `useApi.js` → `useApi.ts` (106 LOC → 120 LOC, types: `ApiResponse<T>`, `ApiCallOptions`, ajout `put()`)
|
||||
- [x] `useCustomFields.js` → `useCustomFields.ts` (105 LOC, type: `CustomFieldValue`)
|
||||
- [x] `useMachineTypesApi.js` → `useMachineTypesApi.ts` (173 → 188 LOC, types: `MachineType`, `MachineTypeRequirement`)
|
||||
- [x] `useMachines.js` → `useMachines.ts` (267 LOC, type: `Machine`, utilise `extractCollection`)
|
||||
- **Fichiers deja TS :** `useProducts.ts`, `usePieces.ts`, `useComposants.ts`, `useConstructeurs.ts`, `useSites.ts`, `useDocuments.ts`
|
||||
- **Fichiers JS restants (deprecated) :** `useComponentModels.js`, `usePieceModels.js` (stubs deprecated, a supprimer)
|
||||
- **Notes :** `ApiResponse<T = any>` par defaut `any` pour backward-compat. Les callers existants fonctionnent sans changement ; le nouveau code peut opt-in strict via `get<MyType>()`.
|
||||
|
||||
### F3.3 Eliminer les `any` restants
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichiers concernes :**
|
||||
- `components/ProductSelect.vue` — 1 `any` restant (slot template, incompressible)
|
||||
- `components/model-types/ManagementView.vue` — remplace `data?: any` → `Record<string, unknown>`, `error: any` → `error: unknown`, `item: any` → `item: unknown`
|
||||
- `components/ComponentStructureAssignmentNode.vue` — 12 casts `(definition as any).typePiece/typeProduct` elimines grace a l'extension des types
|
||||
- `components/ComponentModelStructureEditor.vue` — `Promise<any>` → `Promise<unknown>`
|
||||
- `components/model-types/ModelTypeForm.vue` — `(incoming as any).description` → cast `Record<string, unknown>`
|
||||
- `shared/types/inventory.ts` — `ComponentModelPiece.typePiece?` et `ComponentModelProduct.typeProduct?` ajoutes, 3 casts `(value as any)` supprimes
|
||||
- **Probleme :** 20+ usages de `any` type identifies.
|
||||
- **Action :** Etendre les interfaces de types pour supporter les formes alternatives de l'API. Remplacer les `any` par `unknown` ou `Record<string, unknown>` la ou possible.
|
||||
- **Agent :** Claude
|
||||
- **Notes :** ~15 casts `any` elimines. Les `Record<string, any>` restants dans ComponentModelStructureEditor sont justifies (manipulation dynamique interne de custom fields). Typecheck 0 erreurs.
|
||||
|
||||
---
|
||||
|
||||
## Phase F4 - Qualite du code frontend
|
||||
|
||||
> **Priorite :** MOYENNE
|
||||
|
||||
### F4.1 Activer les regles ESLint critiques
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichier :** `Inventory_frontend/eslint.config.mjs`
|
||||
- **Probleme :** Presque toutes les regles etaient desactivees (`no-console: off`, `no-unused-vars: off`, `no-explicit-any: off`).
|
||||
- **Action realisee :**
|
||||
1. [x] Active `@typescript-eslint/no-explicit-any: warn` (526 warnings — amelioration progressive)
|
||||
2. [x] Active `no-console: warn` avec `allow: ['error']` — 0 violations (deja nettoye en F4.2)
|
||||
3. [x] Active `@typescript-eslint/no-unused-vars: warn` avec ignore `^_` — 0 violations (26 corrigees)
|
||||
4. [x] Corrige les 26 violations `no-unused-vars` : imports inutilises supprimes, variables prefixees `_`, destructurations nettoyees
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 16 fichiers modifies. Regles organisees par categorie (vue, console, typescript, formatting). 0 erreurs, 526 warnings `no-explicit-any` restants (warn, pas bloquant).
|
||||
|
||||
### F4.2 Nettoyer les console.log/console.error
|
||||
|
||||
- **Statut :** `[x]` (console.log supprime, console.error conserve)
|
||||
- **Fichiers modifies :** 8 fichiers (useMachineTypesApi.ts, useSites.ts, type/[id].vue, type/edit/[id].vue, TypeEditPieceRequirementsSection.vue, SearchSelect.vue, app.vue)
|
||||
- **Probleme :** 19 appels `console.log` de debug laisses dans le code de production.
|
||||
- **Action :**
|
||||
1. [x] Supprimer les 19 `console.log` de debug (normalizeRequirementList, page loading, route params, etc.)
|
||||
2. [ ] Les 72 `console.error` restants sont conserves (gestion d'erreur legitime). Migration vers un logger centralise a faire en F4.3.
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 0 `console.log/warn/debug/info` restants dans le frontend.
|
||||
|
||||
### F4.3 Centraliser la gestion d'erreurs API
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichier :** `Inventory_frontend/app/composables/useApi.js` (105 LOC)
|
||||
- **Probleme :** Gestion d'erreur basique (juste un toast). Pas de retry, pas d'intercepteur, erreurs silencieuses dans certains composables.
|
||||
- **Action :**
|
||||
1. Ajouter un systeme de retry configurable (1-3 tentatives)
|
||||
2. Centraliser la gestion des erreurs HTTP (401 -> redirect login, 500 -> message explicite)
|
||||
3. Ajouter des intercepteurs request/response
|
||||
4. Uniformiser le pattern dans tous les composables
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase F5 - Reduire le fichier modelUtils.ts (1017 LOC)
|
||||
|
||||
> **Priorite :** MOYENNE
|
||||
|
||||
### F5.1 Decouper `shared/modelUtils.ts`
|
||||
|
||||
- **Statut :** `[x]`
|
||||
- **Fichier :** `Inventory_frontend/app/shared/modelUtils.ts` (1017 LOC → 37 LOC barrel)
|
||||
- **Probleme :** Fichier utilitaire monolithique de 1017 lignes regroupant toute la logique de manipulation de modeles.
|
||||
- **Action :**
|
||||
1. Identifier les groupes de fonctions (structure, custom fields, requirements, serialization)
|
||||
2. Decouper en 3 modules thematiques :
|
||||
- `shared/model/componentStructure.ts` (~590 LOC) — helpers, sanitize, hydrate, normalize, extract, format pour composants
|
||||
- `shared/model/pieceProductStructure.ts` (~155 LOC) — structure piece/produit (clone, sanitize, hydrate, format)
|
||||
- `shared/model/definitionOverrides.ts` (~50 LOC) — sanitization des overrides de definition
|
||||
3. Re-exporter depuis `shared/modelUtils.ts` (barrel) pour ne pas casser les imports
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 11 fichiers consommateurs inchanges (barrel preserve la retro-compat). Typecheck 0 erreurs.
|
||||
|
||||
---
|
||||
|
||||
## Phase F6 - Tests frontend
|
||||
|
||||
> **Priorite :** HAUTE - Aucun test actuellement
|
||||
|
||||
### F6.1 Configurer Vitest
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichiers crees :**
|
||||
- `vitest.config.ts` — config Vitest avec happy-dom, alias `~` et `#imports`
|
||||
- `tests/__mocks__/imports.ts` — mock des auto-imports Nuxt (useRuntimeConfig, useRoute, etc.)
|
||||
- `tests/shared/inventory-types.test.ts` — 9 tests smoke (validator, empty structures)
|
||||
- **Action realisee :**
|
||||
1. [x] Installe `vitest`, `@vue/test-utils`, `happy-dom`
|
||||
2. [x] Configure Vitest avec environment happy-dom et resolution d'alias
|
||||
3. [x] Ajoute scripts `test` et `test:watch` dans `package.json`
|
||||
4. [x] Premier test suite : `componentModelStructureValidator` (9 tests, 100% pass)
|
||||
- **Agent :** Claude
|
||||
- **Notes :** `npm test` → 9 tests, 0 failures, <1s. Alias `#imports` pointe vers un mock minimal extensible.
|
||||
|
||||
### F6.2 Tests unitaires des composables
|
||||
|
||||
- **Statut :** `[x]` DONE (base)
|
||||
- **Fichiers crees :**
|
||||
- `tests/shared/apiHelpers.test.ts` — 10 tests (extractCollection, tous formats API)
|
||||
- `tests/shared/modelUtils.test.ts` — 18 tests (isPlainObject, clone, stats, format, piece/product)
|
||||
- `tests/shared/inventory-types.test.ts` — 9 tests (validator, empty structures)
|
||||
- `tests/composables/useToast.test.ts` — 9 tests (add, types, max limit, clear, singleton)
|
||||
- `tests/composables/useConfirm.test.ts` — 8 tests (open, confirm, cancel, options, singleton)
|
||||
- **Action realisee :**
|
||||
1. [x] Teste `extractCollection()` : array, hydra:member, member, items, data, null, undefined
|
||||
2. [x] Teste `useToast` : ajout, types, max 3 toasts, clearAll, removeToast, singleton
|
||||
3. [x] Teste `useConfirm` : open/close, resolve true/false, custom options, singleton state
|
||||
4. [x] Teste `modelUtils` : clone, stats, preview, isPlainObject, piece/product variants
|
||||
5. [x] Teste `componentModelStructureValidator` : valid/invalid, custom fields, subcomponents
|
||||
- **Agent :** Claude
|
||||
- **Notes :** 54 tests, 5 fichiers, 100% pass, <2s. Tests `useApi` et CRUD composables necessitent mock fetch (phase ulterieure).
|
||||
|
||||
### F6.3 Tests de composants
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Fichiers a creer :**
|
||||
- `tests/components/Pagination.test.ts`
|
||||
- `tests/components/SearchSelect.test.ts`
|
||||
- `tests/components/MachineHeader.test.ts` (apres F1.1)
|
||||
- **Action :**
|
||||
1. Tester les composants communs (Pagination, SearchSelect)
|
||||
2. Tester le rendu conditionnel et les events
|
||||
- **Agent :** -
|
||||
- **Notes :** -
|
||||
|
||||
---
|
||||
|
||||
## Phase F7 - Ameliorations UX/DX
|
||||
|
||||
> **Priorite :** BASSE - Polish
|
||||
|
||||
### F7.1 Reduire le props drilling
|
||||
|
||||
- **Statut :** `[ ]`
|
||||
- **Probleme :** Props passees sur 3+ niveaux (ex: machine data dans les sous-composants).
|
||||
- **Action :**
|
||||
1. Identifier les cas de props drilling >2 niveaux
|
||||
2. Utiliser `provide/inject` ou des composables partages
|
||||
3. Documenter le pattern choisi
|
||||
- **Agent :** -
|
||||
- **Notes :** A traiter apres F1 (decoupage des composants).
|
||||
|
||||
### F7.2 Remplacer `confirm()` natif par des modales DaisyUI
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Probleme :** Les confirmations de suppression utilisaient `window.confirm()` (UI native, non-stylee).
|
||||
- **Action realisee :**
|
||||
1. [x] Cree `composables/useConfirm.ts` — composable promise-based avec etat reactif partage
|
||||
2. [x] Cree `components/common/ConfirmModal.vue` — modale DaisyUI teleportee (backdrop blur, btn-error)
|
||||
3. [x] Monte `ConfirmModal` globalement dans `app.vue`
|
||||
4. [x] Remplace les 10 `confirm()` natifs dans 10 fichiers :
|
||||
- `constructeurs.vue`, `profiles/manage.vue`, `ManagementView.vue`
|
||||
- `product-catalog.vue`, `index.vue`, `machines/index.vue`
|
||||
- `machine-skeleton/index.vue`, `pieces-catalog.vue`, `component-catalog.vue`
|
||||
- `useSiteManagement.ts` (composable — import explicite)
|
||||
- **Agent :** Claude
|
||||
- **Notes :** API : `const { confirm } = useConfirm(); const ok = await confirm({ message: '...' })`. Auto-import Nuxt pour les SFC, import explicite pour les composables.
|
||||
|
||||
### F7.3 Nettoyer `app.vue` (861 LOC)
|
||||
|
||||
- **Statut :** `[x]` DONE
|
||||
- **Fichier :** `Inventory_frontend/app/app.vue` (861 → 49 LOC)
|
||||
- **Probleme :** Le fichier racine contenait le layout principal, la navbar (~676 LOC dupliquee mobile/desktop), et du state management.
|
||||
- **Action realisee :**
|
||||
1. Cree `composables/useNavDropdown.ts` (~65 LOC) — gestion etat dropdowns navbar
|
||||
2. Cree `components/layout/AppNavbar.vue` (~310 LOC) — navbar data-driven avec `v-for` eliminant duplication mobile/desktop
|
||||
3. `app.vue` reecrit en orchestrateur minimal (49 LOC) + converti en TypeScript
|
||||
4. Supprime 4 imports d'icones inutilises
|
||||
- **Agent :** Claude
|
||||
- **Notes :** Approche data-driven : liens et groupes definis comme tableaux types (`NavLink[]`, `NavGroup[]`), rendus par `v-for` pour mobile et desktop
|
||||
|
||||
---
|
||||
|
||||
## Ordre d'execution recommande
|
||||
|
||||
```
|
||||
=== BACKEND === === FRONTEND ===
|
||||
|
||||
Phase 6.1 (Tests unitaires) Phase F6.1 (Config Vitest)
|
||||
| |
|
||||
v v
|
||||
Phase 1 (Securite) Phase F1 (Decoupage mega-composants)
|
||||
| |
|
||||
v v
|
||||
Phase 2 (Duplication backend) Phase F2 (Duplication frontend)
|
||||
| |
|
||||
v v
|
||||
Phase 3 (Controllers) Phase F3 (Migration TypeScript)
|
||||
| |
|
||||
v v
|
||||
Phase 6.2 (Tests API) Phase F4 (Qualite code) + Phase F5 (modelUtils)
|
||||
| |
|
||||
v v
|
||||
Phase 4 (Stockage) Phase F6.2-F6.3 (Tests frontend)
|
||||
| |
|
||||
v v
|
||||
Phase 5 + Phase 7 (Nettoyage) Phase F7 (UX/DX polish)
|
||||
|
|
||||
v
|
||||
Phase 6.3 (Tests audit)
|
||||
```
|
||||
|
||||
> Les colonnes backend et frontend peuvent etre executees **en parallele** par des agents differents.
|
||||
|
||||
---
|
||||
|
||||
## Journal des modifications
|
||||
|
||||
| Date | Phase | Tache | Agent | Statut | Notes |
|
||||
| ---------- | ----- | ------------------------- | --------------- | ------- | ---------------------------------------------- |
|
||||
| 2026-02-03 | - | Creation du plan backend | Claude Opus 4.5 | Termine | Analyse initiale backend (7 phases, 17 taches) |
|
||||
| 2026-02-03 | - | Creation du plan frontend | Claude Opus 4.5 | Termine | Analyse frontend (7 phases, 22 taches) |
|
||||
| | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## Commandes de verification
|
||||
|
||||
> **Contexte :** Le backend tourne dans Docker (`docker compose`), le frontend est en local.
|
||||
> Les commandes ci-dessous sont executees **depuis la racine du projet** (`/home/matthieu/dev_malio/Inventory/`).
|
||||
|
||||
### Frontend (Nuxt 3 / Vue 3 / TypeScript)
|
||||
|
||||
| Commande | Description | Quand l'utiliser |
|
||||
| -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `npx nuxi typecheck` | Verification des types TypeScript via `vue-tsc` | Apres chaque modification de fichier `.vue` ou `.ts`. C'est la commande principale de validation. |
|
||||
| `npm run lint` | ESLint (config dans `eslint.config.mjs`) | Apres chaque modification pour verifier le style et les erreurs statiques. |
|
||||
| `npm run lint:fix` | ESLint avec auto-fix | Pour corriger automatiquement les erreurs de formatage. |
|
||||
| `npm run build` | Build de production Nuxt (inclut le typecheck) | Avant un commit pour s'assurer que tout compile. Plus lent que `typecheck` seul. |
|
||||
| `npx nuxi prepare` | Regenerer les types auto-generes (`.nuxt/`) | Si les imports auto (composables, components) ne sont pas reconnus par le typecheck. |
|
||||
|
||||
> **Toutes les commandes frontend** sont executees depuis `Inventory_frontend/` :
|
||||
>
|
||||
> ```bash
|
||||
> cd Inventory_frontend && npx nuxi typecheck
|
||||
> ```
|
||||
|
||||
> **Note sur les erreurs pre-existantes :** Il y a ~120 erreurs TypeScript pre-existantes documentees
|
||||
> (anterieures a la refacto). L'objectif est de ne pas en ajouter de nouvelles.
|
||||
> Pour verifier : comparer le nombre d'erreurs avant/apres modification.
|
||||
|
||||
### Backend (Symfony 8 / PHP 8.4)
|
||||
|
||||
| Commande | Description | Quand l'utiliser |
|
||||
| ---------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `vendor/bin/php-cs-fixer fix --dry-run --diff` | Verifie le style PHP (PSR-12 + Symfony) sans modifier | Apres chaque modification PHP. |
|
||||
| `vendor/bin/php-cs-fixer fix` | Corrige automatiquement le style PHP | Avant chaque commit. |
|
||||
| `bin/phpunit` | Lance les tests PHPUnit | Apres chaque modification backend. |
|
||||
| `php bin/console cache:clear` | Vide le cache Symfony | Si des erreurs bizarres apparaissent apres un changement de config. |
|
||||
|
||||
> **Les commandes backend** sont executees **dans le conteneur Docker** :
|
||||
>
|
||||
> ```bash
|
||||
> docker compose exec web vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
> docker compose exec web bin/phpunit
|
||||
> ```
|
||||
|
||||
### Workflow de verification (checklist par tache)
|
||||
|
||||
```
|
||||
1. Lire les fichiers concernes (AVANT toute modification)
|
||||
2. Effectuer les modifications
|
||||
3. Frontend : npx nuxi typecheck → verifier pas de nouvelles erreurs
|
||||
4. Frontend : npm run lint:fix → corriger le formatage
|
||||
5. Backend : php-cs-fixer fix → corriger le style PHP
|
||||
6. Backend : bin/phpunit → verifier la non-regression
|
||||
7. Commit si tout est OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regles pour les agents
|
||||
|
||||
1. **Avant de commencer une tache :**
|
||||
- Mettre le statut a `[~]` dans ce fichier
|
||||
- Inscrire son nom/ID dans la colonne "Agent"
|
||||
- Lire les fichiers concernes AVANT de modifier quoi que ce soit
|
||||
|
||||
2. **Pendant le travail :**
|
||||
- Ne modifier QUE les fichiers listes dans la tache
|
||||
- Respecter les conventions existantes (PSR-12, strict_types)
|
||||
- Ne pas introduire de nouvelles dependances sans justification
|
||||
- Lancer `php-cs-fixer` apres les modifications
|
||||
|
||||
3. **Apres avoir termine :**
|
||||
- Mettre le statut a `[x]`
|
||||
- Ajouter une entree dans le "Journal des modifications"
|
||||
- Lancer les tests existants (`make test`) pour verifier la non-regression
|
||||
- Decrire brievement les changements effectues dans "Notes"
|
||||
|
||||
4. **En cas de blocage :**
|
||||
- Mettre le statut a `[!]`
|
||||
- Documenter le blocage dans "Notes"
|
||||
- Ne PAS passer a une autre tache sans signaler le blocage
|
||||
|
||||
5. **Regles specifiques au frontend :**
|
||||
- Ecrire en TypeScript (pas de JS pour les nouveaux fichiers)
|
||||
- Pas de `any` - utiliser des types concrets
|
||||
- Pas de `console.log` - utiliser le logger ou `useToast`
|
||||
- Composants Vue : max 400 LOC par fichier
|
||||
- Utiliser les composants DaisyUI existants (pas de CSS custom)
|
||||
- Tester avec Vitest quand la config est en place
|
||||
|
||||
6. **Regles specifiques au backend :**
|
||||
- `declare(strict_types=1)` obligatoire
|
||||
- Respecter PSR-12 + regles Symfony (php-cs-fixer)
|
||||
- Pas de `exec()` direct - utiliser Symfony Process
|
||||
- Tester avec PHPUnit
|
||||
138
RELEASE.md
Normal file
138
RELEASE.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Guide de Release
|
||||
|
||||
## Versioning
|
||||
|
||||
Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH`
|
||||
|
||||
- **MAJOR** : Changements incompatibles avec les versions précédentes
|
||||
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
|
||||
- **PATCH** : Corrections de bugs rétrocompatibles
|
||||
|
||||
La version est centralisée dans le fichier `VERSION` à la racine du projet.
|
||||
|
||||
## Créer une release
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Tous les changements doivent être commités
|
||||
- Les tests doivent passer
|
||||
- Être sur la branche à releaser (ex: `main`, `develop`)
|
||||
|
||||
### Utilisation du script
|
||||
|
||||
```bash
|
||||
# Afficher l'aide et la version actuelle
|
||||
./scripts/release.sh
|
||||
|
||||
# Bump patch : 1.0.0 → 1.0.1
|
||||
./scripts/release.sh patch
|
||||
|
||||
# Bump minor : 1.0.0 → 1.1.0
|
||||
./scripts/release.sh minor
|
||||
|
||||
# Bump major : 1.0.0 → 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`
|
||||
|
||||
### Pousser la release
|
||||
|
||||
```bash
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
### 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. Titre : `v1.0.0` (ou avec un nom descriptif)
|
||||
5. Description : résumé des changements (voir section Notes de release)
|
||||
|
||||
## Notes de release
|
||||
|
||||
Template pour les notes de release :
|
||||
|
||||
```markdown
|
||||
## Nouveautés
|
||||
- Feature A
|
||||
- Feature B
|
||||
|
||||
## Corrections
|
||||
- Fix du bug X
|
||||
- Fix du bug Y
|
||||
|
||||
## Changements
|
||||
- Refactoring de Z
|
||||
- Mise à jour des dépendances
|
||||
```
|
||||
|
||||
## Fichiers impactés par le versioning
|
||||
|
||||
| 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 }}` |
|
||||
|
||||
## 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 :
|
||||
```bash
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php bin/console cache:clear --env=prod
|
||||
php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
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)
|
||||
2
TODO.md
Normal file
2
TODO.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- Doc: ne pas oublier de mettre `make` dans la documentation.
|
||||
- Note: le probleme d'IP sous WSL, a ajouter dans la doc.
|
||||
@@ -11,7 +11,7 @@ fi
|
||||
|
||||
# Types autorisés (MINUSCULES uniquement)
|
||||
# Optionnel: scope => feat(auth) : ...
|
||||
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|wip)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||
|
||||
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||
echo "❌ Message de commit invalide."
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"doctrine/doctrine-bundle": "^3.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||
"doctrine/orm": "^3.6",
|
||||
"lexik/jwt-authentication-bundle": "^3.2",
|
||||
"nelmio/cors-bundle": "^2.6",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
@@ -27,8 +28,10 @@
|
||||
"symfony/security-bundle": "8.0.*",
|
||||
"symfony/serializer": "8.0.*",
|
||||
"symfony/twig-bundle": "8.0.*",
|
||||
"symfony/uid": "8.0.*",
|
||||
"symfony/validator": "8.0.*",
|
||||
"symfony/yaml": "8.0.*"
|
||||
"symfony/yaml": "8.0.*",
|
||||
"vich/uploader-bundle": "^2.9"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
||||
540
composer.lock
generated
540
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bab4560dec1d36eec0b0aa2284bd8559",
|
||||
"content-hash": "9e0e35659f9b6ef5c0a60262a36b61f2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
@@ -2361,6 +2361,259 @@
|
||||
},
|
||||
"time": "2025-10-26T09:35:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jms/metadata",
|
||||
"version": "2.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/metadata.git",
|
||||
"reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330",
|
||||
"reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/cache": "^1.0|^2.0",
|
||||
"doctrine/coding-standard": "^8.0",
|
||||
"mikey179/vfsstream": "^1.6.7",
|
||||
"phpunit/phpunit": "^8.5.42|^9.6.23",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Metadata\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Johannes M. Schmitt",
|
||||
"email": "schmittjoh@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Class/method/property metadata management in PHP",
|
||||
"keywords": [
|
||||
"annotations",
|
||||
"metadata",
|
||||
"xml",
|
||||
"yaml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/metadata/issues",
|
||||
"source": "https://github.com/schmittjoh/metadata/tree/2.9.0"
|
||||
},
|
||||
"time": "2025-11-30T20:12:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/jwt",
|
||||
"version": "5.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lcobucci/jwt.git",
|
||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"ext-sodium": "*",
|
||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"infection/infection": "^0.29",
|
||||
"lcobucci/clock": "^3.2",
|
||||
"lcobucci/coding-standard": "^11.0",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.2",
|
||||
"phpstan/phpstan": "^1.10.7",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.5.0",
|
||||
"phpunit/phpunit": "^11.1"
|
||||
},
|
||||
"suggest": {
|
||||
"lcobucci/clock": ">= 3.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lcobucci\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luís Cobucci",
|
||||
"email": "lcobucci@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
|
||||
"keywords": [
|
||||
"JWS",
|
||||
"jwt"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lcobucci/jwt/issues",
|
||||
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/lcobucci",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/lcobucci",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-17T11:30:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lexik/jwt-authentication-bundle",
|
||||
"version": "v3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
|
||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"lcobucci/jwt": "^5.0",
|
||||
"php": ">=8.2",
|
||||
"symfony/clock": "^6.4|^7.0|^8.0",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.4|^3.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation-contracts": "^1.0|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"api-platform/core": "^3.0|^4.0",
|
||||
"rector/rector": "^1.2",
|
||||
"symfony/browser-kit": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
||||
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
|
||||
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeremy Barthe",
|
||||
"email": "j.barthe@lexik.fr",
|
||||
"homepage": "https://github.com/jeremyb"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Cabot",
|
||||
"email": "n.cabot@lexik.fr",
|
||||
"homepage": "https://github.com/slashfan"
|
||||
},
|
||||
{
|
||||
"name": "Cedric Girard",
|
||||
"email": "c.girard@lexik.fr",
|
||||
"homepage": "https://github.com/cedric-g"
|
||||
},
|
||||
{
|
||||
"name": "Dev Lexik",
|
||||
"email": "dev@lexik.fr",
|
||||
"homepage": "https://github.com/lexik"
|
||||
},
|
||||
{
|
||||
"name": "Robin Chalas",
|
||||
"email": "robin.chalas@gmail.com",
|
||||
"homepage": "https://github.com/chalasr"
|
||||
},
|
||||
{
|
||||
"name": "Lexik Community",
|
||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "This bundle provides JWT authentication for your Symfony REST API",
|
||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"JWS",
|
||||
"api",
|
||||
"bundle",
|
||||
"jwt",
|
||||
"rest",
|
||||
"symfony"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
|
||||
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/chalasr",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-20T17:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/cors-bundle",
|
||||
"version": "2.6.0",
|
||||
@@ -4613,6 +4866,92 @@
|
||||
],
|
||||
"time": "2025-12-31T09:29:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v8.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40",
|
||||
"reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"symfony/polyfill-intl-idn": "^1.10",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"egulias/email-validator": "~3.0.0",
|
||||
"phpdocumentor/reflection-docblock": "<3.2.2",
|
||||
"phpdocumentor/type-resolver": "<1.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3.1|^4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
|
||||
"symfony/dependency-injection": "^7.4|^8.0",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/property-access": "^7.4|^8.0",
|
||||
"symfony/property-info": "^7.4|^8.0",
|
||||
"symfony/serializer": "^7.4|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Mime\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows manipulating MIME messages",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"mime",
|
||||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v8.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-16T10:17:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/password-hasher",
|
||||
"version": "v8.0.0",
|
||||
@@ -4768,6 +5107,93 @@
|
||||
],
|
||||
"time": "2025-06-27T09:58:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"symfony/polyfill-intl-normalizer": "^1.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Idn\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Laurent Bassin",
|
||||
"email": "laurent@bassin.info"
|
||||
},
|
||||
{
|
||||
"name": "Trevor Rowbotham",
|
||||
"email": "trevor.rowbotham@pm.me"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"idn",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-10T14:38:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
@@ -7040,6 +7466,114 @@
|
||||
],
|
||||
"time": "2025-12-14T11:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vich/uploader-bundle",
|
||||
"version": "v2.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dustin10/VichUploaderBundle.git",
|
||||
"reference": "945939a04a33c0b78c5fbb7ead31533d85112df5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dustin10/VichUploaderBundle/zipball/945939a04a33c0b78c5fbb7ead31533d85112df5",
|
||||
"reference": "945939a04a33c0b78c5fbb7ead31533d85112df5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/persistence": "^3.0 || ^4.0",
|
||||
"ext-simplexml": "*",
|
||||
"jms/metadata": "^2.4",
|
||||
"php": "^8.1",
|
||||
"symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/event-dispatcher-contracts": "^3.1",
|
||||
"symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/mime": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/property-access": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/string": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.12",
|
||||
"league/flysystem": "<2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dg/bypass-finals": "^1.9",
|
||||
"doctrine/common": "^3.0",
|
||||
"doctrine/doctrine-bundle": "^2.7 || ^3.0",
|
||||
"doctrine/mongodb-odm": "^2.4",
|
||||
"doctrine/orm": "^2.13 || ^3.0",
|
||||
"ext-sqlite3": "*",
|
||||
"knplabs/knp-gaufrette-bundle": "dev-master",
|
||||
"league/flysystem-bundle": "^2.4 || ^3.0",
|
||||
"league/flysystem-memory": "^2.0 || ^3.0",
|
||||
"matthiasnoback/symfony-dependency-injection-test": "^5.1 || ^6.0",
|
||||
"mikey179/vfsstream": "^1.6.11",
|
||||
"phpunit/phpunit": "^10.5 || ^11.5 || ^12.2",
|
||||
"symfony/asset": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/browser-kit": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/dom-crawler": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/phpunit-bridge": "^7.3",
|
||||
"symfony/security-csrf": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/validator": "^5.4.22 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/doctrine-bundle": "For integration with Doctrine",
|
||||
"doctrine/mongodb-odm-bundle": "For integration with Doctrine ODM",
|
||||
"doctrine/orm": "For integration with Doctrine ORM",
|
||||
"doctrine/phpcr-odm": "For integration with Doctrine PHPCR",
|
||||
"knplabs/knp-gaufrette-bundle": "For integration with Gaufrette",
|
||||
"league/flysystem-bundle": "For integration with Flysystem",
|
||||
"liip/imagine-bundle": "To generate image thumbnails",
|
||||
"oneup/flysystem-bundle": "For integration with Flysystem",
|
||||
"symfony/asset": "To generate better links",
|
||||
"symfony/form": "To handle uploads in forms",
|
||||
"symfony/yaml": "To use YAML mapping"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Vich\\UploaderBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dustin Dobervich",
|
||||
"email": "ddobervich@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Ease file uploads attached to entities",
|
||||
"homepage": "https://github.com/dustin10/VichUploaderBundle",
|
||||
"keywords": [
|
||||
"file uploads",
|
||||
"upload"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dustin10/VichUploaderBundle/issues",
|
||||
"source": "https://github.com/dustin10/VichUploaderBundle/tree/v2.9.1"
|
||||
},
|
||||
"time": "2025-12-10T08:23:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "2.0.0",
|
||||
@@ -10208,7 +10742,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -10216,6 +10750,6 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
FrameworkBundle::class => ['all' => true],
|
||||
TwigBundle::class => ['all' => true],
|
||||
SecurityBundle::class => ['all' => true],
|
||||
DoctrineBundle::class => ['all' => true],
|
||||
DoctrineMigrationsBundle::class => ['all' => true],
|
||||
NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatformBundle::class => ['all' => true],
|
||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
api_platform:
|
||||
title: Hello API Platform
|
||||
version: 1.0.0
|
||||
version: 1.4.0
|
||||
defaults:
|
||||
stateless: true
|
||||
stateless: false
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
pagination_items_per_page: 30
|
||||
pagination_maximum_items_per_page: 200
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
mapping_types:
|
||||
modelcategory: string
|
||||
_text: string
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
@@ -9,7 +12,8 @@ doctrine:
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
orm:
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
naming_strategy: doctrine.orm.naming_strategy.default
|
||||
quote_strategy: doctrine.orm.quote_strategy.default
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
|
||||
4
config/packages/lexik_jwt_authentication.yaml
Normal file
4
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
lexik_jwt_authentication:
|
||||
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
@@ -4,7 +4,8 @@ nelmio_cors:
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_credentials: true
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
paths:
|
||||
'^/': null
|
||||
'^/api/': ~
|
||||
|
||||
@@ -2,30 +2,60 @@ security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
App\Entity\Profile:
|
||||
algorithm: auto
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\Profile
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||
security: false
|
||||
|
||||
login:
|
||||
pattern: ^/api/login_check
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
json_login:
|
||||
check_path: /api/login_check
|
||||
username_path: email
|
||||
password_path: password
|
||||
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||
|
||||
session_profile:
|
||||
pattern: ^/api/session
|
||||
stateless: false
|
||||
|
||||
session_api:
|
||||
pattern: ^/api/(sites|machines|documents|profiles)
|
||||
stateless: false
|
||||
|
||||
api:
|
||||
pattern: ^/api
|
||||
stateless: false
|
||||
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
|
||||
# Activate different ways to authenticate:
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
provider: app_user_provider
|
||||
|
||||
# Note: Only the *first* matching rule is applied
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/api/session/profile, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/session/profiles, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/test, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/docs, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/contexts, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/\.well-known, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
@@ -768,6 +768,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* property?: scalar|null|Param, // Default: null
|
||||
* manager_name?: scalar|null|Param, // Default: null
|
||||
* },
|
||||
* lexik_jwt?: array{
|
||||
* class?: scalar|null|Param, // Default: "Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\User\\JWTUser"
|
||||
* },
|
||||
* }>,
|
||||
* firewalls: array<string, array{ // Default: []
|
||||
* pattern?: scalar|null|Param,
|
||||
@@ -826,6 +829,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* provider?: scalar|null|Param,
|
||||
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
||||
* },
|
||||
* jwt?: array{
|
||||
* provider?: scalar|null|Param, // Default: null
|
||||
* authenticator?: scalar|null|Param, // Default: "lexik_jwt_authentication.security.jwt_authenticator"
|
||||
* },
|
||||
* login_link?: array{
|
||||
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
||||
@@ -1514,6 +1521,91 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* ...<mixed>
|
||||
* },
|
||||
* }
|
||||
* @psalm-type LexikJwtAuthenticationConfig = array{
|
||||
* public_key?: scalar|null|Param, // The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key. // Default: null
|
||||
* additional_public_keys?: list<scalar|null|Param>,
|
||||
* secret_key?: scalar|null|Param, // The key used to sign tokens. It can be a raw secret (for HMAC), a raw RSA/ECDSA key or the path to a file itself being plaintext or PEM. // Default: null
|
||||
* pass_phrase?: scalar|null|Param, // The key passphrase (useless for HMAC) // Default: ""
|
||||
* token_ttl?: scalar|null|Param, // Default: 3600
|
||||
* allow_no_expiration?: bool|Param, // Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare. // Default: false
|
||||
* clock_skew?: scalar|null|Param, // Default: 0
|
||||
* encoder?: array{
|
||||
* service?: scalar|null|Param, // Default: "lexik_jwt_authentication.encoder.lcobucci"
|
||||
* signature_algorithm?: scalar|null|Param, // Default: "RS256"
|
||||
* },
|
||||
* user_id_claim?: scalar|null|Param, // Default: "username"
|
||||
* token_extractors?: array{
|
||||
* authorization_header?: bool|array{
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* prefix?: scalar|null|Param, // Default: "Bearer"
|
||||
* name?: scalar|null|Param, // Default: "Authorization"
|
||||
* },
|
||||
* cookie?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* name?: scalar|null|Param, // Default: "BEARER"
|
||||
* },
|
||||
* query_parameter?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* name?: scalar|null|Param, // Default: "bearer"
|
||||
* },
|
||||
* split_cookie?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* cookies?: list<scalar|null|Param>,
|
||||
* },
|
||||
* },
|
||||
* remove_token_from_body_when_cookies_used?: scalar|null|Param, // Default: true
|
||||
* set_cookies?: array<string, array{ // Default: []
|
||||
* lifetime?: scalar|null|Param, // The cookie lifetime. If null, the "token_ttl" option value will be used // Default: null
|
||||
* samesite?: "none"|"lax"|"strict"|Param, // Default: "lax"
|
||||
* path?: scalar|null|Param, // Default: "/"
|
||||
* domain?: scalar|null|Param, // Default: null
|
||||
* secure?: scalar|null|Param, // Default: true
|
||||
* httpOnly?: scalar|null|Param, // Default: true
|
||||
* partitioned?: scalar|null|Param, // Default: false
|
||||
* split?: list<scalar|null|Param>,
|
||||
* }>,
|
||||
* api_platform?: bool|array{ // API Platform compatibility: add check_path in OpenAPI documentation.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* check_path?: scalar|null|Param, // The login check path to add in OpenAPI. // Default: null
|
||||
* username_path?: scalar|null|Param, // The path to the username in the JSON body. // Default: null
|
||||
* password_path?: scalar|null|Param, // The path to the password in the JSON body. // Default: null
|
||||
* },
|
||||
* access_token_issuance?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* signature?: array{
|
||||
* algorithm: scalar|null|Param, // The algorithm use to sign the access tokens.
|
||||
* key: scalar|null|Param, // The signature key. It shall be JWK encoded.
|
||||
* },
|
||||
* encryption?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* key_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||
* content_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
||||
* key: scalar|null|Param, // The encryption key. It shall be JWK encoded.
|
||||
* },
|
||||
* },
|
||||
* access_token_verification?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* signature?: array{
|
||||
* header_checkers?: list<scalar|null|Param>,
|
||||
* claim_checkers?: list<scalar|null|Param>,
|
||||
* mandatory_claims?: list<scalar|null|Param>,
|
||||
* allowed_algorithms?: list<scalar|null|Param>,
|
||||
* keyset: scalar|null|Param, // The signature keyset. It shall be JWKSet encoded.
|
||||
* },
|
||||
* encryption?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* continue_on_decryption_failure?: bool|Param, // If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted. // Default: false
|
||||
* header_checkers?: list<scalar|null|Param>,
|
||||
* allowed_key_encryption_algorithms?: list<scalar|null|Param>,
|
||||
* allowed_content_encryption_algorithms?: list<scalar|null|Param>,
|
||||
* keyset: scalar|null|Param, // The encryption keyset. It shall be JWKSet encoded.
|
||||
* },
|
||||
* },
|
||||
* blocklist_token?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* cache?: scalar|null|Param, // Storage to track blocked tokens // Default: "cache.app"
|
||||
* },
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1525,6 +1617,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* "when@dev"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1536,6 +1629,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1548,6 +1642,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* },
|
||||
* "when@test"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1560,6 +1655,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* api_platform?: ApiPlatformConfig,
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
@@ -7,5 +7,8 @@
|
||||
# To list all registered routes, run the following command:
|
||||
# bin/console debug:router
|
||||
|
||||
api_login_check:
|
||||
path: /api/login_check
|
||||
|
||||
controllers:
|
||||
resource: routing.controllers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
api_platform:
|
||||
resource: .
|
||||
type: api_platform
|
||||
prefix: /
|
||||
prefix: /api
|
||||
|
||||
5
config/routes/routing.controllers.yaml
Normal file
5
config/routes/routing.controllers.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
@@ -21,3 +21,15 @@ services:
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
App\EventSubscriber\ProductAuditSubscriber:
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
App\EventSubscriber\PieceAuditSubscriber:
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
App\EventSubscriber\ComposantAuditSubscriber:
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
61
create_test_user.php
Normal file
61
create_test_user.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
|
||||
|
||||
// Hash the password
|
||||
$factory = new PasswordHasherFactory([
|
||||
'common' => ['algorithm' => 'bcrypt'],
|
||||
'memory-hard' => ['algorithm' => 'argon2i'],
|
||||
]);
|
||||
|
||||
$passwordHasher = $factory->getPasswordHasher('common');
|
||||
$hashedPassword = $passwordHasher->hash('admin123');
|
||||
|
||||
// Connect to database
|
||||
$pdo = new PDO(
|
||||
'pgsql:host=db;port=5432;dbname=inventory',
|
||||
'root',
|
||||
'root'
|
||||
);
|
||||
|
||||
// Check if table exists
|
||||
$tableExists = $pdo->query("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'profiles')")->fetchColumn();
|
||||
|
||||
if ($tableExists) {
|
||||
echo "Table 'profiles' exists.\n";
|
||||
|
||||
// Check if user exists
|
||||
$userExists = $pdo->prepare('SELECT COUNT(*) FROM profiles WHERE email = ?');
|
||||
$userExists->execute(['admin@admin.com']);
|
||||
|
||||
if ($userExists->fetchColumn() > 0) {
|
||||
echo "User admin@admin.com already exists. Updating password...\n";
|
||||
$stmt = $pdo->prepare('UPDATE profiles SET password = ? WHERE email = ?');
|
||||
$stmt->execute([$hashedPassword, 'admin@admin.com']);
|
||||
echo "Password updated!\n";
|
||||
} else {
|
||||
echo "Creating user admin@admin.com...\n";
|
||||
$stmt = $pdo->prepare('INSERT INTO profiles (id, email, first_name, last_name, is_active, roles, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())');
|
||||
$id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
||||
$stmt->execute([
|
||||
$id,
|
||||
'admin@admin.com',
|
||||
'Admin',
|
||||
'User',
|
||||
true,
|
||||
json_encode(['ROLE_USER', 'ROLE_ADMIN']),
|
||||
$hashedPassword,
|
||||
]);
|
||||
echo "User created!\n";
|
||||
}
|
||||
} else {
|
||||
echo "Table 'profiles' does not exist yet. Run migrations first.\n";
|
||||
}
|
||||
|
||||
echo "\nTest credentials:\n";
|
||||
echo "Email: admin@admin.com\n";
|
||||
echo "Password: admin123\n";
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
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"
|
||||
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN}
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||
@@ -29,8 +30,8 @@ services:
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "3000:3000"
|
||||
- "8081:80"
|
||||
- "3001:3000"
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
@@ -41,7 +42,20 @@ services:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
- "${POSTGRES_PORT:-5433}:5432"
|
||||
restart: unless-stopped
|
||||
|
||||
adminer:
|
||||
container_name: adminer-${DOCKER_APP_NAME}
|
||||
image: adminer:latest
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: db
|
||||
ADMINER_DESIGN: dracula
|
||||
ports:
|
||||
- "${ADMINER_PORT:-5050}:8080"
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
DOCKER_APP_NAME=ferme
|
||||
DOCKER_APP_NAME=inventory
|
||||
DOCKER_PHP_VERSION=8.4.6
|
||||
DOCKER_NODE_VERSION=24.12.0
|
||||
APP_USER=www-data
|
||||
POSTGRES_DB=ferme
|
||||
POSTGRES_DB=inventory
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5432
|
||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||
35
docker/.env.docker.local
Normal file
35
docker/.env.docker.local
Normal file
@@ -0,0 +1,35 @@
|
||||
DOCKER_APP_NAME=inventory
|
||||
DOCKER_PHP_VERSION=8.4.6
|
||||
DOCKER_NODE_VERSION=24.12.0
|
||||
APP_USER=www-data
|
||||
CURRENT_UID=1000
|
||||
CURRENT_GID=1000
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_DB=inventory
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
#
|
||||
# CORS
|
||||
CORS_ALLOW_ORIGIN=^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?$
|
||||
POSTGRES_PORT=5433
|
||||
|
||||
# pgAdmin
|
||||
PGADMIN_EMAIL=admin@admin.com
|
||||
PGADMIN_PASSWORD=admin
|
||||
PGADMIN_PORT=5050
|
||||
|
||||
# XDebug
|
||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||
|
||||
# Symfony (pour future migration)
|
||||
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_change_me
|
||||
|
||||
# NestJS
|
||||
NESTJS_PORT=3000
|
||||
SESSION_SECRET=changeme_session_secret
|
||||
CORS_ORIGIN=http://localhost:3001
|
||||
2
docker/pgadmin/pgpass
Normal file
2
docker/pgadmin/pgpass
Normal file
@@ -0,0 +1,2 @@
|
||||
db:5432:inventory:root:root
|
||||
db:5432:*:root:root
|
||||
15
docker/pgadmin/servers.json
Normal file
15
docker/pgadmin/servers.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Servers": {
|
||||
"1": {
|
||||
"Name": "Inventory PostgreSQL",
|
||||
"Group": "Servers",
|
||||
"Host": "db",
|
||||
"Port": 5432,
|
||||
"MaintenanceDB": "inventory",
|
||||
"Username": "root",
|
||||
"SSLMode": "prefer",
|
||||
"PassFile": "/var/lib/pgadmin/pgpass",
|
||||
"Comment": "Serveur PostgreSQL du projet Inventory"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
git \
|
||||
unzip \
|
||||
qpdf \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
intl \
|
||||
zip \
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html
|
||||
ServerName localhost
|
||||
DocumentRoot /var/www/html/public
|
||||
|
||||
AliasMatch "^/api(/.*)?" "/var/www/html/public$1"
|
||||
# API Symfony
|
||||
<Directory /var/www/html/public>
|
||||
Options FollowSymLinks
|
||||
Options +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
AliasMatch "^(/.*)?" "/var/www/html/frontend/dist$1"
|
||||
<Directory /var/www/html/frontend/dist>
|
||||
AllowOverride All
|
||||
Order allow,deny
|
||||
Allow from All
|
||||
|
||||
RewriteEngine on
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
RewriteRule ^ index.html [L]
|
||||
</Directory>
|
||||
|
||||
ErrorLog "${APACHE_LOG_DIR}/error.log"
|
||||
CustomLog "${APACHE_LOG_DIR}/access.log" combined
|
||||
# Logs
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
|
||||
1092
fixtures/data.sql
Normal file
1092
fixtures/data.sql
Normal file
File diff suppressed because one or more lines are too long
42
fixtures/load.sh
Executable file
42
fixtures/load.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Load fixtures into the database
|
||||
# Usage: ./fixtures/load.sh [--reset]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Load environment variables
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
export $(grep -v '^#' "$PROJECT_DIR/.env" | xargs)
|
||||
fi
|
||||
|
||||
DB_USER="${POSTGRES_USER:-root}"
|
||||
DB_NAME="${POSTGRES_DB:-inventory}"
|
||||
CONTAINER="${DB_CONTAINER:-inventory-db-1}"
|
||||
|
||||
echo "Loading fixtures into $DB_NAME..."
|
||||
|
||||
# Check if --reset flag is passed
|
||||
if [ "$1" == "--reset" ]; then
|
||||
echo "Resetting database (truncating all tables)..."
|
||||
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c "
|
||||
DO \$\$ DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP
|
||||
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END \$\$;
|
||||
"
|
||||
fi
|
||||
|
||||
# Load fixtures with foreign key checks disabled
|
||||
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" <<EOF
|
||||
SET session_replication_role = replica;
|
||||
$(cat "$SCRIPT_DIR/data.sql")
|
||||
SET session_replication_role = DEFAULT;
|
||||
EOF
|
||||
|
||||
echo "Fixtures loaded successfully!"
|
||||
24
frontend/.gitignore
vendored
24
frontend/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -1,75 +0,0 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<NuxtPage/>
|
||||
</template>
|
||||
@@ -1,9 +0,0 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
ssr: false,
|
||||
modules: ['@nuxtjs/tailwindcss'],
|
||||
typescript: {
|
||||
strict: true
|
||||
}
|
||||
})
|
||||
11892
frontend/package-lock.json
generated
11892
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^4.2.2",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<h1 class="text-3xl font-bold">Nuxt OK ✅</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
makefile
47
makefile
@@ -19,6 +19,8 @@ EXEC_PHP_ROOT = $(DOCKER) exec -t -u root $(PHP_CONTAINER)
|
||||
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
|
||||
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
|
||||
FILES =
|
||||
DATA_SQL ?= data.sql
|
||||
DATA_SQL_NORM ?= data_norm.sql
|
||||
|
||||
#========================================================================================
|
||||
|
||||
@@ -31,6 +33,11 @@ start: env-init
|
||||
@echo "**** START CONTAINERS ****"
|
||||
@cp --update=none docker/.env.docker docker/.env.docker.local
|
||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||
@echo ""
|
||||
@echo "URLs disponibles:"
|
||||
@echo "- Symfony API: http://localhost:8081/api"
|
||||
@echo "- Nuxt (Inventory_frontend): http://localhost:3001"
|
||||
@echo "- pgAdmin: http://localhost:5050"
|
||||
|
||||
# Éteint le container
|
||||
stop:
|
||||
@@ -49,16 +56,16 @@ composer-install:
|
||||
$(EXEC_PHP) composer install
|
||||
|
||||
build-nuxtJS:
|
||||
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
||||
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
||||
# $(EXEC_PHP) cp -n Inventory_frontend/.env.dist Inventory_frontend/.env.local
|
||||
$(EXEC_PHP) sh -lc "cd Inventory_frontend && npm install && npm run generate"
|
||||
|
||||
dev-nuxt:
|
||||
$(EXEC_PHP) sh -c "cd frontend && npm run dev"
|
||||
$(EXEC_PHP) sh -lc "cd Inventory_frontend && npm run dev"
|
||||
|
||||
delete_built_dir:
|
||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf frontend/node_modules
|
||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf Inventory_frontend/node_modules
|
||||
|
||||
remove_orphans:
|
||||
$(DOCKER_COMPOSE) kill
|
||||
@@ -85,6 +92,10 @@ db-restart:
|
||||
cache-clear:
|
||||
$(SYMFONY_CONSOLE) cache:clear
|
||||
|
||||
cache-clear-full:
|
||||
$(SYMFONY_CONSOLE) cache:clear
|
||||
$(EXEC_PHP) rm -rf var/cache/*
|
||||
|
||||
copy-git-hook:
|
||||
$(EXEC_PHP) cp pre-commit .git/hooks/
|
||||
$(EXEC_PHP) cp commit-msg .git/hooks/
|
||||
@@ -108,3 +119,31 @@ test:
|
||||
|
||||
wait:
|
||||
sleep 10
|
||||
|
||||
# Normalize pgAdmin data-only dump and import into DB
|
||||
import-data:
|
||||
python3 scripts/normalize-dump.py $(DATA_SQL) $(DATA_SQL_NORM) --lower
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 < $(DATA_SQL_NORM)
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
|
||||
|
||||
# Fixtures management
|
||||
fixtures-dump:
|
||||
@echo "Dumping current database to fixtures/data.sql..."
|
||||
$(DOCKER_COMPOSE) exec -T db pg_dump -U $(POSTGRES_USER) -d $(POSTGRES_DB) \
|
||||
--data-only --inserts --no-owner --no-privileges \
|
||||
--exclude-table=doctrine_migration_versions \
|
||||
| grep -v "^pg_dump:" | grep -v "^\\\\restrict" > fixtures/data.sql
|
||||
@echo "Fixtures saved to fixtures/data.sql"
|
||||
|
||||
fixtures-load:
|
||||
@echo "Loading fixtures from fixtures/data.sql (FK checks disabled)..."
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = replica;"
|
||||
-$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) < fixtures/data.sql
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = DEFAULT;"
|
||||
@echo "Fixtures loaded!"
|
||||
|
||||
fixtures-reset:
|
||||
@echo "Resetting database and loading fixtures..."
|
||||
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "DO \$$\$$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END \$$\$$;"
|
||||
$(MAKE) fixtures-load
|
||||
|
||||
35
migratebdd.md
Normal file
35
migratebdd.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Migration DB (manuel)
|
||||
|
||||
Ce guide explique comment importer un dump SQL venant de pgAdmin dans la base Docker.
|
||||
|
||||
## 1) Export pgAdmin
|
||||
|
||||
Dans pgAdmin:
|
||||
|
||||
- Format: Plain
|
||||
- Options: Use INSERT commands + Use column inserts
|
||||
- Fichier: `data.sql`
|
||||
|
||||
## 2) Normaliser le dump
|
||||
|
||||
Convertit les colonnes camelCase en lowercase compact.
|
||||
|
||||
```bash
|
||||
python3 scripts/normalize-dump.py data.sql data_norm.sql --lower
|
||||
```
|
||||
|
||||
## 3) Importer dans la base Docker
|
||||
|
||||
Utilise `session_replication_role` pour eviter les erreurs de contraintes circulaires.
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 < data_norm.sql
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
|
||||
```
|
||||
|
||||
## 4) Verifier
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker/.env.docker.local exec -T db psql -U $POSTGRES_USER -d $POSTGRES_DB -c "\\dt"
|
||||
```
|
||||
891
migrations/Version20260125143939.php
Normal file
891
migrations/Version20260125143939.php
Normal file
@@ -0,0 +1,891 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260125143939 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f95a3199df92e79b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f95a3199a3fdb2a7') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_A_fkey"');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_B_fkey"');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _composantconstructeurs ADD PRIMARY KEY (A, B)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_5b97d813e8b7be43') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('_composantconstructeurs_b_index') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff6736d61') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7fff6bae05f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ffa1dac1c6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff96428d73') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ffa3fdb2a7') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c158582c3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378cdf92e79b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c4ca601c8') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c40c2d03b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288f6bae05f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288a1dac1c6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b0728896428d73') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288a3fdb2a7') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288fcf7805f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19f6bae05f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19a1dac1c6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe197d44d2df') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19bcced9e3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_62941615f6bae05f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6294161596428d73') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_629416157d44d2df') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6294161532c54aaf') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('machine_product_links_machineid_idx') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('machine_product_links_productid_idx') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259357fdbff') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc322597d44d2df') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259bcd7dad6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc3225987ceb33f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f1ce8dedfcf7805f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f1ce8ded158582c3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_B_fkey"');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_A_fkey"');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _machineconstructeurs ADD PRIMARY KEY (A, B)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4f225b32e8b7be43') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('_machineconstructeurs_b_index') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_category_name_key"');
|
||||
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_code_key"');
|
||||
$this->addSql('ALTER TABLE model_types ALTER id TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR(255)');
|
||||
$this->addSql('ALTER TABLE model_types ALTER createdAt DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE model_types ALTER componentSkeleton TYPE JSON');
|
||||
$this->addSql('ALTER TABLE model_types ALTER pieceSkeleton TYPE JSON');
|
||||
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b92d74724ca601c8') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b92d7472a3fdb2a7') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_A_fkey"');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_B_fkey"');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs ALTER A TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs ALTER B TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _piececonstructeurs ADD PRIMARY KEY (A, B)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_77fc120e8b7be43') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('_piececonstructeurs_b_index') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b3ba5a5a40c2d03b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey"');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey"');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs ALTER A TYPE VARCHAR(36)');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs ALTER B TYPE VARCHAR(36)');
|
||||
// Clean orphaned relations before re-adding foreign keys.
|
||||
$this->addSql('DELETE FROM _productconstructeurs WHERE A IS NULL OR B IS NULL');
|
||||
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM products p WHERE p.id = pc.A)');
|
||||
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM constructeurs c WHERE c.id = pc.B)');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
|
||||
$this->addSql('ALTER TABLE _productconstructeurs ADD PRIMARY KEY (A, B)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_66f61802e8b7be43') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('_productconstructeurs_b_index') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_96958790158582c3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_96958790df92e79b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f609e59e158582c3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f609e59e4ca601c8') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_29a51f98158582c3') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_29a51f9840c2d03b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER a TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER b TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_607601254ad0cf31') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_60760125d3d99e8b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CCD3D99E8B');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CC4AD0CF31');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER a TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER b TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_e6a040cc4ad0cf31') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_e6a040ccd3d99e8b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E5D3D99E8B');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E54AD0CF31');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER a TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER b TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_e94732e54ad0cf31') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_e94732e5d3d99e8b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FCD3D99E8B');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FC4AD0CF31');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER a TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER b TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_cf7403fc4ad0cf31') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_cf7403fcd3d99e8b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f95a319936799605') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f95a3199cc8a4cee') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff345ee564') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff5c4a705f') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff633ec4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff3c6a9d1') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_6b64d7ff36799605') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c57b7763a') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c2f024c2') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378c169f1cf6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_4a48378ccc8a4cee') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288345ee564') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b07288633ec4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b072886973a4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b072883c6a9d1') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_a2b0728836799605') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19345ee564') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19633ec4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19ef6cf34b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_528efe19c44b383c') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_62941615ef6cf34b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_62941615633ec4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_629416153c6a9d1') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_62941615f957d314') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259633ec4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc3225936799605') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259ef6cf34b') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259b590b209') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259a63ac5dc') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_8cc32259937a1d7c') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f1ce8ded2f024c2') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f1ce8ded6973a4fd') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('ALTER TABLE model_types ALTER id TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR');
|
||||
$this->addSql('ALTER TABLE model_types ALTER componentskeleton TYPE JSONB');
|
||||
$this->addSql('ALTER TABLE model_types ALTER pieceskeleton TYPE JSONB');
|
||||
$this->addSql('ALTER TABLE model_types ALTER productskeleton TYPE JSONB');
|
||||
$this->addSql('ALTER TABLE model_types ALTER createdat SET DEFAULT CURRENT_TIMESTAMP');
|
||||
$this->addSql('CREATE UNIQUE INDEX "ModelType_category_name_key" ON model_types (category, name)');
|
||||
$this->addSql('CREATE UNIQUE INDEX "ModelType_code_key" ON model_types (code)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b92d7472169f1cf6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b92d747236799605') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_b3ba5a5a57b7763a') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_969587902f024c2') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_96958790cc8a4cee') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f609e59e169f1cf6') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_f609e59e2f024c2') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_29a51f9857b7763a') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
$this->addSql(<<<'SQL'
|
||||
DO $$ BEGIN
|
||||
IF to_regclass('idx_29a51f982f024c2') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
}
|
||||
41
migrations/Version20260125170000.php
Normal file
41
migrations/Version20260125170000.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260125170000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add audit_logs table to store per-entity history entries.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE audit_logs (
|
||||
id VARCHAR(36) NOT NULL,
|
||||
entityType VARCHAR(50) NOT NULL,
|
||||
entityId VARCHAR(36) NOT NULL,
|
||||
action VARCHAR(20) NOT NULL,
|
||||
diff JSON DEFAULT NULL,
|
||||
snapshot JSON DEFAULT NULL,
|
||||
actorProfileId VARCHAR(36) DEFAULT NULL,
|
||||
createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql('CREATE INDEX idx_audit_entity ON audit_logs (entityType, entityId)');
|
||||
$this->addSql('CREATE INDEX idx_audit_created_at ON audit_logs (createdAt)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE audit_logs');
|
||||
}
|
||||
}
|
||||
70
public/.htaccess
Normal file
70
public/.htaccess
Normal file
@@ -0,0 +1,70 @@
|
||||
# Use the front controller as index file. It serves as a fallback solution when
|
||||
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||
# feature in your server configuration. Uncomment the following line if you
|
||||
# install assets as symlinks or if you experience problems related to symlinks
|
||||
# when compiling LESS/Sass/CoffeeScript assets.
|
||||
# Options +FollowSymlinks
|
||||
|
||||
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
|
||||
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
|
||||
Options +FollowSymlinks
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Determine the RewriteBase automatically and set it as environment variable.
|
||||
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||
# project in a subdirectory, the base path will be prepended to allow proper
|
||||
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||
# work in environments without path prefix as well, providing a safe, one-size
|
||||
# fits all solution. But as you do not need it in this case, you can comment
|
||||
# the following 2 lines to eliminate the overhead.
|
||||
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||
RewriteCond %{HTTP:Authorization} .+
|
||||
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||
|
||||
# Redirect to URI without front controller to prevent duplicate content
|
||||
# (with and without `/index.php`). Only do this redirect on the initial
|
||||
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||
# endless redirect loop (request -> rewrite to front controller ->
|
||||
# redirect to URI without front controller -> request -> ...).
|
||||
# So in case you get a "too many redirects" error or you always get redirected
|
||||
# to the start page because your Apache does not expose the REDIRECT_STATUS
|
||||
# environment variable, you have 2 choices:
|
||||
# - disable this feature by commenting the following 2 lines or
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
<IfModule mod_alias.c>
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 307 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
5
scripts/insert_profiles.sql
Normal file
5
scripts/insert_profiles.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
INSERT INTO public.profiles (id, firstname, lastname, email, isactive, createdat, updatedat)
|
||||
VALUES
|
||||
('admin-default-profile', 'Admin', 'General', 'admin@admin.fr', true, '2025-09-23 13:09:47.804', '2025-09-23 13:09:47.804'),
|
||||
('cmhab2j3x003g47v77xhnm1ff', 'Elodie', 'Souriau', 'elodie@gg.fr', true, '2025-10-28 08:29:25.437', '2025-10-28 08:29:25.437')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
32
scripts/lowercase-columns.sql
Normal file
32
scripts/lowercase-columns.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND column_name <> lower(column_name)
|
||||
ORDER BY table_name, column_name
|
||||
LOOP
|
||||
-- Skip if a lowercase version already exists to avoid collisions.
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns c
|
||||
WHERE c.table_schema = r.table_schema
|
||||
AND c.table_name = r.table_name
|
||||
AND c.column_name = lower(r.column_name)
|
||||
) THEN
|
||||
RAISE NOTICE 'Skip %.%: % -> % (target exists)', r.table_name, r.column_name, r.column_name, lower(r.column_name);
|
||||
ELSE
|
||||
EXECUTE format(
|
||||
'ALTER TABLE %I.%I RENAME COLUMN %I TO %I',
|
||||
r.table_schema,
|
||||
r.table_name,
|
||||
r.column_name,
|
||||
lower(r.column_name)
|
||||
);
|
||||
RAISE NOTICE 'Renamed %.%: % -> %', r.table_name, r.column_name, r.column_name, lower(r.column_name);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
45
scripts/migrate-inventory-data.sh
Executable file
45
scripts/migrate-inventory-data.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DB="${SOURCE_DB:-inventory_data}"
|
||||
TARGET_DB="${TARGET_DB:-inventory}"
|
||||
PGHOST="${PGHOST:-localhost}"
|
||||
PGPORT="${PGPORT:-5433}"
|
||||
PGUSER="${PGUSER:-postgres}"
|
||||
PGPASSWORD="${PGPASSWORD:-postgres}"
|
||||
DUMP_FILE="${DUMP_FILE:-/tmp/inventory_data_dump.sql}"
|
||||
|
||||
export PGPASSWORD
|
||||
|
||||
EXCLUDE_TABLES=(
|
||||
"doctrine_migration_versions"
|
||||
"migration_versions"
|
||||
"_prisma_migrations"
|
||||
"profiles"
|
||||
)
|
||||
|
||||
EXCLUDE_ARGS=()
|
||||
for table in "${EXCLUDE_TABLES[@]}"; do
|
||||
EXCLUDE_ARGS+=(--exclude-table-data="$table")
|
||||
done
|
||||
|
||||
echo "Dumping data from ${SOURCE_DB}..."
|
||||
pg_dump \
|
||||
--data-only \
|
||||
--inserts \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
"${EXCLUDE_ARGS[@]}" \
|
||||
-h "${PGHOST}" \
|
||||
-p "${PGPORT}" \
|
||||
-U "${PGUSER}" \
|
||||
"${SOURCE_DB}" > "${DUMP_FILE}"
|
||||
|
||||
echo "Restoring data into ${TARGET_DB}..."
|
||||
psql \
|
||||
-h "${PGHOST}" \
|
||||
-p "${PGPORT}" \
|
||||
-U "${PGUSER}" \
|
||||
"${TARGET_DB}" < "${DUMP_FILE}"
|
||||
|
||||
echo "Done. Data copied from ${SOURCE_DB} to ${TARGET_DB}."
|
||||
177
scripts/normalize-dump.py
Normal file
177
scripts/normalize-dump.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
INSERT_RE = re.compile(
|
||||
r"(?P<prefix>INSERT\s+INTO\s+[^;]*?\()(?P<cols>[^)]*)(?P<suffix>\)\s+VALUES)",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
TABLE_RE = re.compile(
|
||||
r"(?P<before>INSERT\s+INTO\s+)(?P<table>(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?\"[^\"]+\"|(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?[A-Za-z_][\w$]*)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
CREATE_DB_RE = re.compile(r"^CREATE\s+DATABASE\s+.+?;$", re.IGNORECASE | re.MULTILINE)
|
||||
CONNECT_RE = re.compile(r"^\\connect\\s+.+?$", re.IGNORECASE | re.MULTILINE)
|
||||
|
||||
|
||||
TABLE_NAME_MAP = {
|
||||
"ModelType": "model_types",
|
||||
"TypeMachine": "type_machines",
|
||||
"TypeMachineComponentRequirement": "type_machine_component_requirements",
|
||||
"TypeMachinePieceRequirement": "type_machine_piece_requirements",
|
||||
"TypeMachineProductRequirement": "type_machine_product_requirements",
|
||||
"MachinePieceLink": "machine_piece_links",
|
||||
"MachineComponentLink": "machine_component_links",
|
||||
"MachineProductLink": "machine_product_links",
|
||||
"Machine": "machines",
|
||||
"Product": "products",
|
||||
"Piece": "pieces",
|
||||
"Composant": "composants",
|
||||
"Profile": "profiles",
|
||||
"CustomField": "custom_fields",
|
||||
"CustomFieldValue": "custom_field_values",
|
||||
"Document": "documents",
|
||||
"Constructeur": "constructeurs",
|
||||
"Site": "sites",
|
||||
}
|
||||
|
||||
|
||||
SKIP_TABLES = {
|
||||
"_prisma_migrations",
|
||||
}
|
||||
|
||||
|
||||
def to_snake(name: str) -> str:
|
||||
out = []
|
||||
length = len(name)
|
||||
for i, ch in enumerate(name):
|
||||
if ch.isupper():
|
||||
prev = name[i - 1] if i > 0 else ""
|
||||
nxt = name[i + 1] if i + 1 < length else ""
|
||||
if i > 0 and (prev.islower() or prev.isdigit() or (prev.isupper() and nxt.islower())):
|
||||
out.append("_")
|
||||
out.append(ch.lower())
|
||||
else:
|
||||
out.append(ch)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def to_lower_compact(name: str) -> str:
|
||||
return name.replace("_", "").lower()
|
||||
|
||||
|
||||
def remap_table(ident: str, mode: str) -> str:
|
||||
mapped = TABLE_NAME_MAP.get(ident)
|
||||
if mapped is not None:
|
||||
return mapped
|
||||
if mode == "snake":
|
||||
return to_snake(ident)
|
||||
if mode == "lower":
|
||||
return to_lower_compact(ident)
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
|
||||
|
||||
def extract_table_ident(prefix: str) -> str | None:
|
||||
match = TABLE_RE.search(prefix)
|
||||
if not match:
|
||||
return None
|
||||
table = match.group("table")
|
||||
# Handle quoted schema like "public"."TableName"
|
||||
if '"."' in table:
|
||||
parts = table.split('"."', 1)
|
||||
ident = parts[1].strip('"')
|
||||
elif "." in table:
|
||||
_, ident = table.split(".", 1)
|
||||
ident = ident.strip('"')
|
||||
else:
|
||||
ident = table.strip('"')
|
||||
return ident
|
||||
|
||||
|
||||
def normalize_table_name(prefix: str, mode: str) -> str:
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
table = match.group("table")
|
||||
schema = ""
|
||||
ident = table
|
||||
# Handle quoted schema like "public"."TableName"
|
||||
if '"."' in table:
|
||||
parts = table.split('"."', 1)
|
||||
schema_name = parts[0].strip('"')
|
||||
ident = parts[1].strip('"')
|
||||
schema = f'"{schema_name}".'
|
||||
elif "." in table:
|
||||
schema_part, ident = table.split(".", 1)
|
||||
schema_name = schema_part.strip('"')
|
||||
schema = f'"{schema_name}".'
|
||||
ident = ident.strip('"')
|
||||
else:
|
||||
ident = table.strip('"')
|
||||
mapped = remap_table(ident, mode)
|
||||
return f'{match.group("before")}{schema}"{mapped}"'
|
||||
|
||||
return TABLE_RE.sub(repl, prefix)
|
||||
|
||||
|
||||
def remap_columns(cols: str, mode: str) -> str:
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
name = match.group(1)
|
||||
if mode == "snake":
|
||||
if any(ch.isupper() for ch in name):
|
||||
return f"\"{to_snake(name)}\""
|
||||
return match.group(0)
|
||||
if mode == "lower":
|
||||
return f"\"{to_lower_compact(name)}\""
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
|
||||
return re.sub(r"\"([^\"]+)\"", repl, cols)
|
||||
|
||||
|
||||
def normalize_dump(sql: str, mode: str) -> str:
|
||||
sql = CREATE_DB_RE.sub("", sql)
|
||||
sql = CONNECT_RE.sub("", sql)
|
||||
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
raw_prefix = match.group("prefix")
|
||||
ident = extract_table_ident(raw_prefix)
|
||||
if ident is not None:
|
||||
mapped = remap_table(ident, mode)
|
||||
if ident in SKIP_TABLES or mapped in SKIP_TABLES:
|
||||
return ""
|
||||
prefix = normalize_table_name(raw_prefix, mode)
|
||||
cols = remap_columns(match.group("cols"), mode)
|
||||
return f"{prefix}{cols}{match.group('suffix')}"
|
||||
|
||||
return INSERT_RE.sub(repl, sql)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) not in (3, 4):
|
||||
print("Usage: scripts/normalize-dump.py <input.sql> <output.sql> [--snake|--lower]", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
src = sys.argv[1]
|
||||
dst = sys.argv[2]
|
||||
mode = "lower"
|
||||
if len(sys.argv) == 4:
|
||||
if sys.argv[3] == "--snake":
|
||||
mode = "snake"
|
||||
elif sys.argv[3] == "--lower":
|
||||
mode = "lower"
|
||||
else:
|
||||
print("Invalid mode. Use --snake or --lower.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with open(src, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
|
||||
normalized = normalize_dump(data, mode)
|
||||
|
||||
with open(dst, "w", encoding="utf-8") as f:
|
||||
f.write(normalized)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
195
scripts/release.sh
Executable file
195
scripts/release.sh
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Couleurs pour l'affichage
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Répertoire racine du projet
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
VERSION_FILE="$PROJECT_ROOT/VERSION"
|
||||
API_PLATFORM_FILE="$PROJECT_ROOT/config/packages/api_platform.yaml"
|
||||
FRONTEND_DIR="$PROJECT_ROOT/Inventory_frontend"
|
||||
|
||||
# Lire la version actuelle
|
||||
current_version=$(cat "$VERSION_FILE" | tr -d '\n')
|
||||
|
||||
# Fonction pour afficher l'aide
|
||||
show_help() {
|
||||
echo -e "${BLUE}Usage:${NC} $0 [version|bump_type]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " version Version spécifique (ex: 1.2.3)"
|
||||
echo " bump_type Type de bump: major, minor, patch"
|
||||
echo ""
|
||||
echo "Exemples:"
|
||||
echo " $0 1.0.0 # Définit la version à 1.0.0"
|
||||
echo " $0 patch # 1.0.0 -> 1.0.1"
|
||||
echo " $0 minor # 1.0.0 -> 1.1.0"
|
||||
echo " $0 major # 1.0.0 -> 2.0.0"
|
||||
echo ""
|
||||
echo -e "Version actuelle: ${GREEN}$current_version${NC}"
|
||||
}
|
||||
|
||||
# Fonction pour bumper la version
|
||||
bump_version() {
|
||||
local version=$1
|
||||
local bump_type=$2
|
||||
|
||||
IFS='.' read -r major minor patch <<< "$version"
|
||||
|
||||
case $bump_type in
|
||||
major)
|
||||
major=$((major + 1))
|
||||
minor=0
|
||||
patch=0
|
||||
;;
|
||||
minor)
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
;;
|
||||
patch)
|
||||
patch=$((patch + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$major.$minor.$patch"
|
||||
}
|
||||
|
||||
# Vérifier les arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
arg=$1
|
||||
|
||||
# Déterminer la nouvelle version
|
||||
case $arg in
|
||||
major|minor|patch)
|
||||
new_version=$(bump_version "$current_version" "$arg")
|
||||
;;
|
||||
-h|--help|help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# Vérifier le format de version (semver basique)
|
||||
if [[ $arg =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
new_version=$arg
|
||||
else
|
||||
echo -e "${RED}Erreur:${NC} Format de version invalide: $arg"
|
||||
echo "Utilisez le format X.Y.Z (ex: 1.2.3) ou major/minor/patch"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${BLUE}Release v$new_version${NC}"
|
||||
echo "================================"
|
||||
echo -e "Version actuelle: ${YELLOW}$current_version${NC}"
|
||||
echo -e "Nouvelle version: ${GREEN}$new_version${NC}"
|
||||
echo ""
|
||||
|
||||
# Vérifier qu'on n'est pas sur une version identique
|
||||
if [ "$current_version" = "$new_version" ]; then
|
||||
echo -e "${YELLOW}La version est déjà $new_version${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Demander confirmation
|
||||
read -p "Continuer ? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Annulé."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 1 : Gérer le submodule frontend
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo -e "${BLUE}[1/6]${NC} Vérification du submodule frontend..."
|
||||
|
||||
cd "$FRONTEND_DIR"
|
||||
|
||||
# Vérifier s'il y a des changements non commités dans le submodule
|
||||
if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; then
|
||||
echo -e "${YELLOW}Changements détectés dans le submodule frontend${NC}"
|
||||
git status --short
|
||||
echo ""
|
||||
read -p "Commiter ces changements dans le submodule ? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git add -A
|
||||
git commit -m "chore(release) : prepare v$new_version"
|
||||
else
|
||||
echo -e "${RED}Erreur:${NC} Veuillez d'abord commiter les changements du submodule."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 2 : Tag le submodule
|
||||
# ===========================================
|
||||
echo -e "${BLUE}[2/6]${NC} Création du tag v$new_version dans le submodule..."
|
||||
|
||||
# Vérifier si le tag existe déjà
|
||||
if git rev-parse "v$new_version" >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Le tag v$new_version existe déjà dans le submodule${NC}"
|
||||
else
|
||||
git tag -a "v$new_version" -m "Release v$new_version"
|
||||
echo -e "${GREEN}Tag v$new_version créé dans le submodule${NC}"
|
||||
fi
|
||||
|
||||
# Retourner au projet principal
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 3 : Mettre à jour VERSION
|
||||
# ===========================================
|
||||
echo -e "${BLUE}[3/6]${NC} Mise à jour du fichier VERSION..."
|
||||
echo "$new_version" > "$VERSION_FILE"
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 4 : Mettre à jour api_platform.yaml
|
||||
# ===========================================
|
||||
echo -e "${BLUE}[4/6]${NC} Mise à jour de api_platform.yaml..."
|
||||
sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 5 : Commit principal (avec mise à jour du submodule)
|
||||
# ===========================================
|
||||
echo -e "${BLUE}[5/6]${NC} Création du commit principal..."
|
||||
git add "$VERSION_FILE" "$API_PLATFORM_FILE" "$FRONTEND_DIR"
|
||||
git commit -m "chore(release) : v$new_version"
|
||||
|
||||
# ===========================================
|
||||
# ÉTAPE 6 : Tag principal
|
||||
# ===========================================
|
||||
echo -e "${BLUE}[6/6]${NC} Création du tag v$new_version..."
|
||||
git tag -a "v$new_version" -m "Release v$new_version"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Release v$new_version préparée avec succès !${NC}"
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo -e "${YELLOW}Prochaines étapes :${NC}"
|
||||
echo ""
|
||||
echo "1. Pousser le submodule frontend :"
|
||||
echo -e " ${BLUE}cd Inventory_frontend && git push && git push --tags && cd ..${NC}"
|
||||
echo ""
|
||||
echo "2. Pousser le projet principal :"
|
||||
echo -e " ${BLUE}git push && git push --tags${NC}"
|
||||
echo ""
|
||||
echo "3. Créer les releases sur Gitea :"
|
||||
echo " - Inventory_frontend : tag v$new_version"
|
||||
echo " - Inventory (backend) : tag v$new_version"
|
||||
echo ""
|
||||
echo "================================"
|
||||
139
scripts/validate-migration.php
Normal file
139
scripts/validate-migration.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function parseDatabaseUrl(string $url): array
|
||||
{
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) {
|
||||
throw new RuntimeException('Invalid database URL.');
|
||||
}
|
||||
|
||||
$host = $parts['host'] ?? 'localhost';
|
||||
$port = isset($parts['port']) ? (int) $parts['port'] : 5432;
|
||||
$user = $parts['user'] ?? '';
|
||||
$pass = $parts['pass'] ?? '';
|
||||
$db = ltrim($parts['path'] ?? '', '/');
|
||||
|
||||
return [
|
||||
'dsn' => sprintf('pgsql:host=%s;port=%d;dbname=%s', $host, $port, $db),
|
||||
'user' => $user,
|
||||
'pass' => $pass,
|
||||
];
|
||||
}
|
||||
|
||||
function connect(string $url): PDO
|
||||
{
|
||||
$config = parseDatabaseUrl($url);
|
||||
$pdo = new PDO($config['dsn'], $config['user'], $config['pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
$sourceUrl = getenv('SOURCE_DATABASE_URL') ?: '';
|
||||
$targetUrl = getenv('TARGET_DATABASE_URL') ?: '';
|
||||
|
||||
if ($sourceUrl === '' || $targetUrl === '') {
|
||||
fwrite(STDERR, "Usage: SOURCE_DATABASE_URL=... TARGET_DATABASE_URL=... php scripts/validate-migration.php\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$tables = [
|
||||
'sites',
|
||||
'type_machines',
|
||||
'machines',
|
||||
'model_types',
|
||||
'composants',
|
||||
'pieces',
|
||||
'products',
|
||||
'constructeurs',
|
||||
'documents',
|
||||
'custom_fields',
|
||||
'custom_field_values',
|
||||
'machine_component_links',
|
||||
'machine_piece_links',
|
||||
'machine_product_links',
|
||||
'type_machine_component_requirements',
|
||||
'type_machine_piece_requirements',
|
||||
'type_machine_product_requirements',
|
||||
'_machineconstructeurs',
|
||||
'_composantconstructeurs',
|
||||
'_piececonstructeurs',
|
||||
'_productconstructeurs',
|
||||
'profiles',
|
||||
];
|
||||
|
||||
$skipTables = array_filter(array_map('trim', explode(',', getenv('SKIP_TABLES') ?: 'profiles')));
|
||||
|
||||
$sourceTableMap = [
|
||||
'model_types' => ['ModelType', 'model_types'],
|
||||
'_machineconstructeurs' => ['_MachineConstructeurs', '_machineconstructeurs'],
|
||||
'_composantconstructeurs' => ['_ComposantConstructeurs', '_composantconstructeurs'],
|
||||
'_piececonstructeurs' => ['_PieceConstructeurs', '_piececonstructeurs'],
|
||||
'_productconstructeurs' => ['_ProductConstructeurs', '_productconstructeurs'],
|
||||
];
|
||||
|
||||
function resolveTable(PDO $db, array $candidates): ?string
|
||||
{
|
||||
foreach ($candidates as $candidate) {
|
||||
$exists = (bool) $db
|
||||
->query(sprintf("SELECT to_regclass('public.%s')", $candidate))
|
||||
->fetchColumn();
|
||||
if ($exists) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
$quoted = (bool) $db
|
||||
->query(sprintf("SELECT to_regclass('public.\"%s\"')", $candidate))
|
||||
->fetchColumn();
|
||||
if ($quoted) {
|
||||
return sprintf('"%s"', $candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$source = connect($sourceUrl);
|
||||
$target = connect($targetUrl);
|
||||
|
||||
$hasDifferences = false;
|
||||
|
||||
foreach ($tables as $table) {
|
||||
if (in_array($table, $skipTables, true)) {
|
||||
continue;
|
||||
}
|
||||
$sourceCandidates = $sourceTableMap[$table] ?? [$table];
|
||||
$sourceTable = resolveTable($source, $sourceCandidates);
|
||||
$sourceExists = $sourceTable !== null;
|
||||
$targetExists = (bool) $target
|
||||
->query(sprintf("SELECT to_regclass('public.%s')", $table))
|
||||
->fetchColumn();
|
||||
|
||||
if (!$sourceExists || !$targetExists) {
|
||||
$hasDifferences = true;
|
||||
printf(
|
||||
"%s: source=%s target=%s\n",
|
||||
$table,
|
||||
$sourceExists ? 'exists' : 'missing',
|
||||
$targetExists ? 'exists' : 'missing'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceCount = (int) $source->query(sprintf('SELECT COUNT(*) FROM public.%s', $sourceTable))->fetchColumn();
|
||||
$targetCount = (int) $target->query(sprintf('SELECT COUNT(*) FROM public.%s', $table))->fetchColumn();
|
||||
|
||||
if ($sourceCount !== $targetCount) {
|
||||
$hasDifferences = true;
|
||||
printf("%s: source=%d target=%d\n", $table, $sourceCount, $targetCount);
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasDifferences) {
|
||||
exit(2);
|
||||
}
|
||||
|
||||
echo "Counts match for all tables.\n";
|
||||
175
src/Command/CompressPdfCommand.php
Normal file
175
src/Command/CompressPdfCommand.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\DocumentRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:compress-pdf',
|
||||
description: 'Compress all PDF documents stored in database without quality loss',
|
||||
)]
|
||||
class CompressPdfCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DocumentRepository $documentRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be compressed without actually doing it')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
// Check if qpdf is installed
|
||||
exec('which qpdf', $qpdfPath, $returnCode);
|
||||
if (0 !== $returnCode) {
|
||||
$io->error('qpdf is not installed. Run: sudo apt install qpdf');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['mimeType' => 'application/pdf']);
|
||||
|
||||
if (empty($documents)) {
|
||||
$io->info('No PDF documents found.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->title('PDF Compression');
|
||||
$io->text(sprintf('Found %d PDF documents', count($documents)));
|
||||
|
||||
$totalSaved = 0;
|
||||
$compressed = 0;
|
||||
|
||||
foreach ($documents as $document) {
|
||||
$base64Data = $document->getPath();
|
||||
|
||||
// Remove data URI prefix if present
|
||||
if (str_contains($base64Data, ',')) {
|
||||
$base64Data = explode(',', $base64Data, 2)[1];
|
||||
}
|
||||
|
||||
$pdfContent = base64_decode($base64Data, true);
|
||||
if (false === $pdfContent) {
|
||||
$io->warning(sprintf('Failed to decode document: %s', $document->getName()));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalSize = strlen($pdfContent);
|
||||
|
||||
if ($dryRun) {
|
||||
$io->text(sprintf(
|
||||
' [DRY-RUN] Would compress: %s (%s)',
|
||||
$document->getName(),
|
||||
$this->formatBytes($originalSize)
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create temp files
|
||||
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
|
||||
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
|
||||
|
||||
file_put_contents($tempInput, $pdfContent);
|
||||
|
||||
// Compress with qpdf (lossless)
|
||||
$command = sprintf(
|
||||
'qpdf --linearize --object-streams=generate %s %s 2>&1',
|
||||
escapeshellarg($tempInput),
|
||||
escapeshellarg($tempOutput)
|
||||
);
|
||||
|
||||
exec($command, $cmdOutput, $returnCode);
|
||||
|
||||
if (0 !== $returnCode || !file_exists($tempOutput)) {
|
||||
$io->warning(sprintf('Failed to compress: %s', $document->getName()));
|
||||
@unlink($tempInput);
|
||||
@unlink($tempOutput);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$compressedContent = file_get_contents($tempOutput);
|
||||
$compressedSize = strlen($compressedContent);
|
||||
|
||||
// Only update if we actually saved space
|
||||
if ($compressedSize < $originalSize) {
|
||||
$saved = $originalSize - $compressedSize;
|
||||
$totalSaved += $saved;
|
||||
++$compressed;
|
||||
|
||||
// Rebuild base64 with data URI prefix
|
||||
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
|
||||
$document->setPath($newBase64);
|
||||
$document->setSize($compressedSize);
|
||||
|
||||
$io->text(sprintf(
|
||||
' ✓ %s: %s → %s (-%s, -%.1f%%)',
|
||||
$document->getName(),
|
||||
$this->formatBytes($originalSize),
|
||||
$this->formatBytes($compressedSize),
|
||||
$this->formatBytes($saved),
|
||||
($saved / $originalSize) * 100
|
||||
));
|
||||
} else {
|
||||
$io->text(sprintf(
|
||||
' - %s: Already optimal (%s)',
|
||||
$document->getName(),
|
||||
$this->formatBytes($originalSize)
|
||||
));
|
||||
}
|
||||
|
||||
@unlink($tempInput);
|
||||
@unlink($tempOutput);
|
||||
}
|
||||
|
||||
if (!$dryRun && $compressed > 0) {
|
||||
$this->em->flush();
|
||||
$io->success(sprintf(
|
||||
'Compressed %d/%d PDFs. Total space saved: %s',
|
||||
$compressed,
|
||||
count($documents),
|
||||
$this->formatBytes($totalSaved)
|
||||
));
|
||||
} elseif ($dryRun) {
|
||||
$io->info('Dry run completed. No changes made.');
|
||||
} else {
|
||||
$io->info('No PDFs needed compression.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$i = 0;
|
||||
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||
$bytes /= 1024;
|
||||
++$i;
|
||||
}
|
||||
|
||||
return round($bytes, 2).' '.$units[$i];
|
||||
}
|
||||
}
|
||||
87
src/Controller/ActivityLogController.php
Normal file
87
src/Controller/ActivityLogController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class ActivityLogController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));
|
||||
|
||||
$filters = [];
|
||||
if ($entityType = $request->query->get('entityType')) {
|
||||
$filters['entityType'] = $entityType;
|
||||
}
|
||||
if ($action = $request->query->get('action')) {
|
||||
$filters['action'] = $action;
|
||||
}
|
||||
|
||||
$result = $this->auditLogs->findAllPaginated($page, $itemsPerPage, $filters);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$result['items'],
|
||||
))));
|
||||
|
||||
$actorMap = [];
|
||||
if ([] !== $actorIds) {
|
||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||
foreach ($profiles as $profile) {
|
||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||
if ('' === $label) {
|
||||
$label = $profile->getEmail() ?? $profile->getId();
|
||||
}
|
||||
$actorMap[$profile->getId()] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static function ($log) use ($actorMap) {
|
||||
$actorId = $log->getActorProfileId();
|
||||
$snapshot = $log->getSnapshot();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'entityType' => $log->getEntityType(),
|
||||
'entityId' => $log->getEntityId(),
|
||||
'entityName' => $snapshot['name'] ?? null,
|
||||
'entityRef' => $snapshot['reference'] ?? null,
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $snapshot,
|
||||
];
|
||||
},
|
||||
$result['items'],
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => $result['total'],
|
||||
'page' => $page,
|
||||
'itemsPerPage' => $itemsPerPage,
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
src/Controller/ComposantHistoryController.php
Normal file
79
src/Controller/ComposantHistoryController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class ComposantHistoryController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ComposantRepository $components,
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
|
||||
public function __invoke(string $id): JsonResponse
|
||||
{
|
||||
$component = $this->components->find($id);
|
||||
if (!$component) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Composant introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$logs = $this->auditLogs->findEntityHistory('composant', $id, 200);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$logs,
|
||||
))));
|
||||
|
||||
$actorMap = [];
|
||||
if ([] !== $actorIds) {
|
||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||
foreach ($profiles as $profile) {
|
||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||
if ('' === $label) {
|
||||
$label = $profile->getEmail() ?? $profile->getId();
|
||||
}
|
||||
$actorMap[$profile->getId()] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static function ($log) use ($actorMap) {
|
||||
$actorId = $log->getActorProfileId();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $log->getSnapshot(),
|
||||
];
|
||||
},
|
||||
$logs,
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => count($items),
|
||||
]);
|
||||
}
|
||||
}
|
||||
294
src/Controller/CustomFieldValueController.php
Normal file
294
src/Controller/CustomFieldValueController.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\CustomFieldRepository;
|
||||
use App\Repository\CustomFieldValueRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
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;
|
||||
|
||||
#[Route('/api/custom-fields/values')]
|
||||
class CustomFieldValueController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly CustomFieldRepository $customFieldRepository,
|
||||
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
||||
private readonly MachineRepository $machineRepository,
|
||||
private readonly ComposantRepository $composantRepository,
|
||||
private readonly PieceRepository $pieceRepository,
|
||||
private readonly ProductRepository $productRepository,
|
||||
) {}
|
||||
|
||||
#[Route('', name: 'custom_field_values_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $this->decodePayload($request);
|
||||
if ($payload instanceof JsonResponse) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
$customField = $this->resolveCustomField($payload);
|
||||
if ($customField instanceof JsonResponse) {
|
||||
return $customField;
|
||||
}
|
||||
|
||||
$target = $this->resolveTarget($payload);
|
||||
if ($target instanceof JsonResponse) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$value = new CustomFieldValue();
|
||||
$value->setCustomField($customField);
|
||||
$value->setValue((string) ($payload['value'] ?? ''));
|
||||
$this->applyTarget($value, $target['type'], $target['entity']);
|
||||
|
||||
$this->entityManager->persist($value);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeCustomFieldValue($value));
|
||||
}
|
||||
|
||||
#[Route('/upsert', name: 'custom_field_values_upsert', methods: ['POST'])]
|
||||
public function upsert(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $this->decodePayload($request);
|
||||
if ($payload instanceof JsonResponse) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
$customField = $this->resolveCustomField($payload);
|
||||
if ($customField instanceof JsonResponse) {
|
||||
return $customField;
|
||||
}
|
||||
|
||||
$target = $this->resolveTarget($payload);
|
||||
if ($target instanceof JsonResponse) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$existing = $this->customFieldValueRepository->findOneBy([
|
||||
'customField' => $customField,
|
||||
$target['type'] => $target['entity'],
|
||||
]);
|
||||
|
||||
if ($existing instanceof CustomFieldValue) {
|
||||
$existing->setValue((string) ($payload['value'] ?? ''));
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeCustomFieldValue($existing));
|
||||
}
|
||||
|
||||
$value = new CustomFieldValue();
|
||||
$value->setCustomField($customField);
|
||||
$value->setValue((string) ($payload['value'] ?? ''));
|
||||
$this->applyTarget($value, $target['type'], $target['entity']);
|
||||
|
||||
$this->entityManager->persist($value);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeCustomFieldValue($value));
|
||||
}
|
||||
|
||||
#[Route('/{entityType}/{entityId}', name: 'custom_field_values_list', methods: ['GET'])]
|
||||
public function listByEntity(string $entityType, string $entityId): JsonResponse
|
||||
{
|
||||
$target = $this->resolveTarget([
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entityId,
|
||||
]);
|
||||
|
||||
if ($target instanceof JsonResponse) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$values = $this->customFieldValueRepository->findBy([
|
||||
$target['type'] => $target['entity'],
|
||||
]);
|
||||
|
||||
return $this->json(array_map(
|
||||
fn (CustomFieldValue $value) => $this->normalizeCustomFieldValue($value),
|
||||
$values
|
||||
));
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'custom_field_values_update', methods: ['PATCH'])]
|
||||
public function update(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$value = $this->customFieldValueRepository->find($id);
|
||||
if (!$value instanceof CustomFieldValue) {
|
||||
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = $this->decodePayload($request);
|
||||
if ($payload instanceof JsonResponse) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
if (array_key_exists('value', $payload)) {
|
||||
$value->setValue((string) $payload['value']);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeCustomFieldValue($value));
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'custom_field_values_delete', methods: ['DELETE'])]
|
||||
public function delete(string $id): JsonResponse
|
||||
{
|
||||
$value = $this->customFieldValueRepository->find($id);
|
||||
if (!$value instanceof CustomFieldValue) {
|
||||
return $this->json(['success' => false, 'error' => 'Custom field value not found.'], 404);
|
||||
}
|
||||
|
||||
$this->entityManager->remove($value);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json(['success' => true]);
|
||||
}
|
||||
|
||||
private function decodePayload(Request $request): array|JsonResponse
|
||||
{
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload)) {
|
||||
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function resolveCustomField(array $payload): CustomField|JsonResponse
|
||||
{
|
||||
$customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : '';
|
||||
if ('' !== $customFieldId) {
|
||||
$customField = $this->customFieldRepository->find($customFieldId);
|
||||
if ($customField instanceof CustomField) {
|
||||
return $customField;
|
||||
}
|
||||
|
||||
return $this->json(['success' => false, 'error' => 'Custom field not found.'], 404);
|
||||
}
|
||||
|
||||
$customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : '';
|
||||
if ('' === $customFieldName) {
|
||||
return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400);
|
||||
}
|
||||
|
||||
$customField = new CustomField();
|
||||
$customField->setName($customFieldName);
|
||||
$customField->setType((string) ($payload['customFieldType'] ?? 'text'));
|
||||
$customField->setRequired((bool) ($payload['customFieldRequired'] ?? false));
|
||||
|
||||
$options = $payload['customFieldOptions'] ?? null;
|
||||
if (is_array($options)) {
|
||||
$customField->setOptions($options);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($customField);
|
||||
|
||||
return $customField;
|
||||
}
|
||||
|
||||
private function resolveTarget(array $payload): array|JsonResponse
|
||||
{
|
||||
$entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : '';
|
||||
$entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : '';
|
||||
|
||||
if ('' === $entityType || '' === $entityId) {
|
||||
foreach (['machine', 'composant', 'piece', 'product'] as $candidate) {
|
||||
$key = $candidate.'Id';
|
||||
if (!isset($payload[$key])) {
|
||||
continue;
|
||||
}
|
||||
$entityType = $candidate;
|
||||
$entityId = trim((string) $payload[$key]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ('' === $entityType || '' === $entityId) {
|
||||
return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400);
|
||||
}
|
||||
|
||||
return match ($entityType) {
|
||||
'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository),
|
||||
'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository),
|
||||
'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository),
|
||||
'product' => $this->resolveEntity('product', $entityId, $this->productRepository),
|
||||
default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400),
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveEntity(string $type, string $id, $repository): array|JsonResponse
|
||||
{
|
||||
$entity = $repository->find($id);
|
||||
if (!$entity) {
|
||||
return $this->json(['success' => false, 'error' => sprintf('%s not found.', $type)], 404);
|
||||
}
|
||||
|
||||
return ['type' => $type, 'entity' => $entity];
|
||||
}
|
||||
|
||||
private function applyTarget(CustomFieldValue $value, string $type, object $entity): void
|
||||
{
|
||||
switch ($type) {
|
||||
case 'machine':
|
||||
$value->setMachine($entity);
|
||||
|
||||
break;
|
||||
|
||||
case 'composant':
|
||||
$value->setComposant($entity);
|
||||
|
||||
break;
|
||||
|
||||
case 'piece':
|
||||
$value->setPiece($entity);
|
||||
|
||||
break;
|
||||
|
||||
case 'product':
|
||||
$value->setProduct($entity);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeCustomFieldValue(CustomFieldValue $value): array
|
||||
{
|
||||
$customField = $value->getCustomField();
|
||||
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'value' => $value->getValue(),
|
||||
'customFieldId' => $customField->getId(),
|
||||
'customField' => [
|
||||
'id' => $customField->getId(),
|
||||
'name' => $customField->getName(),
|
||||
'type' => $customField->getType(),
|
||||
'required' => $customField->isRequired(),
|
||||
'options' => $customField->getOptions(),
|
||||
'orderIndex' => $customField->getOrderIndex(),
|
||||
],
|
||||
'machineId' => $value->getMachine()?->getId(),
|
||||
'composantId' => $value->getComposant()?->getId(),
|
||||
'pieceId' => $value->getPiece()?->getId(),
|
||||
'productId' => $value->getProduct()?->getId(),
|
||||
'createdAt' => $value->getCreatedAt()->format(DATE_ATOM),
|
||||
'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM),
|
||||
];
|
||||
}
|
||||
}
|
||||
118
src/Controller/DocumentQueryController.php
Normal file
118
src/Controller/DocumentQueryController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Document;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\DocumentRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\SiteRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/documents')]
|
||||
class DocumentQueryController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DocumentRepository $documentRepository,
|
||||
private readonly SiteRepository $siteRepository,
|
||||
private readonly MachineRepository $machineRepository,
|
||||
private readonly ComposantRepository $composantRepository,
|
||||
private readonly PieceRepository $pieceRepository,
|
||||
private readonly ProductRepository $productRepository,
|
||||
) {}
|
||||
|
||||
#[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])]
|
||||
public function listBySite(string $id): JsonResponse
|
||||
{
|
||||
$site = $this->siteRepository->find($id);
|
||||
if (!$site) {
|
||||
return $this->json(['success' => false, 'error' => 'Site not found.'], 404);
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['site' => $site]);
|
||||
|
||||
return $this->json($this->normalizeDocuments($documents));
|
||||
}
|
||||
|
||||
#[Route('/machine/{id}', name: 'documents_by_machine', methods: ['GET'])]
|
||||
public function listByMachine(string $id): JsonResponse
|
||||
{
|
||||
$machine = $this->machineRepository->find($id);
|
||||
if (!$machine) {
|
||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['machine' => $machine]);
|
||||
|
||||
return $this->json($this->normalizeDocuments($documents));
|
||||
}
|
||||
|
||||
#[Route('/composant/{id}', name: 'documents_by_composant', methods: ['GET'])]
|
||||
public function listByComposant(string $id): JsonResponse
|
||||
{
|
||||
$composant = $this->composantRepository->find($id);
|
||||
if (!$composant) {
|
||||
return $this->json(['success' => false, 'error' => 'Composant not found.'], 404);
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['composant' => $composant]);
|
||||
|
||||
return $this->json($this->normalizeDocuments($documents));
|
||||
}
|
||||
|
||||
#[Route('/piece/{id}', name: 'documents_by_piece', methods: ['GET'])]
|
||||
public function listByPiece(string $id): JsonResponse
|
||||
{
|
||||
$piece = $this->pieceRepository->find($id);
|
||||
if (!$piece) {
|
||||
return $this->json(['success' => false, 'error' => 'Piece not found.'], 404);
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['piece' => $piece]);
|
||||
|
||||
return $this->json($this->normalizeDocuments($documents));
|
||||
}
|
||||
|
||||
#[Route('/product/{id}', name: 'documents_by_product', methods: ['GET'])]
|
||||
public function listByProduct(string $id): JsonResponse
|
||||
{
|
||||
$product = $this->productRepository->find($id);
|
||||
if (!$product) {
|
||||
return $this->json(['success' => false, 'error' => 'Product not found.'], 404);
|
||||
}
|
||||
|
||||
$documents = $this->documentRepository->findBy(['product' => $product]);
|
||||
|
||||
return $this->json($this->normalizeDocuments($documents));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document[] $documents
|
||||
*/
|
||||
private function normalizeDocuments(array $documents): array
|
||||
{
|
||||
return array_map(static function (Document $document): array {
|
||||
return [
|
||||
'id' => $document->getId(),
|
||||
'name' => $document->getName(),
|
||||
'filename' => $document->getFilename(),
|
||||
'path' => $document->getPath(),
|
||||
'mimeType' => $document->getMimeType(),
|
||||
'size' => $document->getSize(),
|
||||
'siteId' => $document->getSite()?->getId(),
|
||||
'machineId' => $document->getMachine()?->getId(),
|
||||
'composantId' => $document->getComposant()?->getId(),
|
||||
'pieceId' => $document->getPiece()?->getId(),
|
||||
'productId' => $document->getProduct()?->getId(),
|
||||
'createdAt' => $document->getCreatedAt()->format(DATE_ATOM),
|
||||
'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM),
|
||||
];
|
||||
}, $documents);
|
||||
}
|
||||
}
|
||||
75
src/Controller/MachineCustomFieldsController.php
Normal file
75
src/Controller/MachineCustomFieldsController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\Machine;
|
||||
use App\Repository\CustomFieldValueRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/machines')]
|
||||
class MachineCustomFieldsController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MachineRepository $machineRepository,
|
||||
private readonly CustomFieldValueRepository $customFieldValueRepository,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])]
|
||||
public function addMissingCustomFields(string $id): JsonResponse
|
||||
{
|
||||
$machine = $this->machineRepository->find($id);
|
||||
if (!$machine instanceof Machine) {
|
||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||
}
|
||||
|
||||
$typeMachine = $machine->getTypeMachine();
|
||||
if (!$typeMachine) {
|
||||
return $this->json(['success' => true, 'machineId' => $machine->getId(), 'customFieldValues' => []]);
|
||||
}
|
||||
|
||||
foreach ($typeMachine->getCustomFields() as $customField) {
|
||||
if (!$customField instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
$existing = $this->customFieldValueRepository->findOneBy([
|
||||
'machine' => $machine,
|
||||
'customField' => $customField,
|
||||
]);
|
||||
if ($existing instanceof CustomFieldValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = new CustomFieldValue();
|
||||
$value->setMachine($machine);
|
||||
$value->setCustomField($customField);
|
||||
$value->setValue($customField->getDefaultValue() ?? '');
|
||||
$this->entityManager->persist($value);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$values = $this->customFieldValueRepository->findBy(['machine' => $machine]);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'machineId' => $machine->getId(),
|
||||
'customFieldValues' => array_map(
|
||||
static fn (CustomFieldValue $value) => [
|
||||
'id' => $value->getId(),
|
||||
'value' => $value->getValue(),
|
||||
'customFieldId' => $value->getCustomField()->getId(),
|
||||
],
|
||||
$values
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
src/Controller/MachineHistoryController.php
Normal file
79
src/Controller/MachineHistoryController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class MachineHistoryController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MachineRepository $machines,
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
|
||||
public function __invoke(string $id): JsonResponse
|
||||
{
|
||||
$machine = $this->machines->find($id);
|
||||
if (!$machine) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Machine introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$logs = $this->auditLogs->findEntityHistory('machine', $id, 200);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$logs,
|
||||
))));
|
||||
|
||||
$actorMap = [];
|
||||
if ([] !== $actorIds) {
|
||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||
foreach ($profiles as $profile) {
|
||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||
if ('' === $label) {
|
||||
$label = $profile->getEmail() ?? $profile->getId();
|
||||
}
|
||||
$actorMap[$profile->getId()] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static function ($log) use ($actorMap) {
|
||||
$actorId = $log->getActorProfileId();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $log->getSnapshot(),
|
||||
];
|
||||
},
|
||||
$logs,
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => count($items),
|
||||
]);
|
||||
}
|
||||
}
|
||||
766
src/Controller/MachineSkeletonController.php
Normal file
766
src/Controller/MachineSkeletonController.php
Normal file
@@ -0,0 +1,766 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\CustomField;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
use App\Entity\MachineProductLink;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\TypeMachineComponentRequirement;
|
||||
use App\Entity\TypeMachinePieceRequirement;
|
||||
use App\Entity\TypeMachineProductRequirement;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use App\Repository\MachineProductLinkRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\TypeMachineComponentRequirementRepository;
|
||||
use App\Repository\TypeMachinePieceRequirementRepository;
|
||||
use App\Repository\TypeMachineProductRequirementRepository;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
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;
|
||||
|
||||
#[Route('/api/machines')]
|
||||
class MachineSkeletonController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MachineRepository $machineRepository,
|
||||
private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
|
||||
private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
|
||||
private readonly MachineProductLinkRepository $machineProductLinkRepository,
|
||||
private readonly ComposantRepository $composantRepository,
|
||||
private readonly PieceRepository $pieceRepository,
|
||||
private readonly ProductRepository $productRepository,
|
||||
private readonly TypeMachineComponentRequirementRepository $componentRequirementRepository,
|
||||
private readonly TypeMachinePieceRequirementRepository $pieceRequirementRepository,
|
||||
private readonly TypeMachineProductRequirementRepository $productRequirementRepository,
|
||||
) {}
|
||||
|
||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])]
|
||||
public function getSkeleton(string $id): JsonResponse
|
||||
{
|
||||
$machine = $this->machineRepository->find($id);
|
||||
if (!$machine instanceof Machine) {
|
||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||
}
|
||||
|
||||
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
||||
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
||||
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
||||
|
||||
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||
$machine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
$productLinks
|
||||
));
|
||||
}
|
||||
|
||||
#[Route('/{id}/skeleton', name: 'machine_skeleton_update', methods: ['PATCH'])]
|
||||
public function updateSkeleton(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$machine = $this->machineRepository->find($id);
|
||||
if (!$machine instanceof Machine) {
|
||||
return $this->json(['success' => false, 'error' => 'Machine not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
if (!is_array($payload)) {
|
||||
return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400);
|
||||
}
|
||||
|
||||
$componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []);
|
||||
$pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []);
|
||||
$productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []);
|
||||
|
||||
$componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload);
|
||||
if ($componentLinks instanceof JsonResponse) {
|
||||
return $componentLinks;
|
||||
}
|
||||
|
||||
$pieceLinks = $this->applyPieceLinks($machine, $pieceLinksPayload, $componentLinks);
|
||||
if ($pieceLinks instanceof JsonResponse) {
|
||||
return $pieceLinks;
|
||||
}
|
||||
|
||||
$productLinks = $this->applyProductLinks($machine, $productLinksPayload, $componentLinks, $pieceLinks);
|
||||
if ($productLinks instanceof JsonResponse) {
|
||||
return $productLinks;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json($this->normalizeMachineSkeletonResponse(
|
||||
$machine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
$productLinks
|
||||
));
|
||||
}
|
||||
|
||||
private function normalizePayloadList(mixed $value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($value, static fn ($item) => is_array($item)));
|
||||
}
|
||||
|
||||
private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse
|
||||
{
|
||||
$existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine]));
|
||||
$keepIds = [];
|
||||
$pendingParents = [];
|
||||
$links = [];
|
||||
|
||||
foreach ($payload as $entry) {
|
||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink();
|
||||
if (!$linkId) {
|
||||
$linkId = $this->generateCuid();
|
||||
}
|
||||
if (!$link->getId()) {
|
||||
$link->setId($linkId);
|
||||
}
|
||||
|
||||
$composantId = $this->resolveIdentifier($entry, ['composantId', 'componentId', 'idComposant']);
|
||||
if (!$composantId) {
|
||||
return $this->json(['success' => false, 'error' => 'Composant requis pour le squelette.'], 400);
|
||||
}
|
||||
$composant = $this->composantRepository->find($composantId);
|
||||
if (!$composant instanceof Composant) {
|
||||
return $this->json(['success' => false, 'error' => 'Composant introuvable.'], 404);
|
||||
}
|
||||
|
||||
$link->setMachine($machine);
|
||||
$link->setComposant($composant);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachineComponentRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->componentRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachineComponentRequirement) {
|
||||
$link->setTypeMachineComponentRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $entry['overrides'] ?? null);
|
||||
|
||||
$pendingParents[$linkId] = $this->resolveIdentifier($entry, [
|
||||
'parentComponentLinkId',
|
||||
'parentLinkId',
|
||||
'parentMachineComponentLinkId',
|
||||
]);
|
||||
|
||||
$this->entityManager->persist($link);
|
||||
$links[$linkId] = $link;
|
||||
$keepIds[] = $linkId;
|
||||
}
|
||||
|
||||
foreach ($pendingParents as $linkId => $parentId) {
|
||||
if (!$parentId) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($links[$linkId])) {
|
||||
continue;
|
||||
}
|
||||
$parent = $links[$parentId] ?? $existing[$parentId] ?? null;
|
||||
if ($parent instanceof MachineComponentLink) {
|
||||
$links[$linkId]->setParentLink($parent);
|
||||
}
|
||||
}
|
||||
|
||||
$this->removeMissingLinks($existing, $keepIds);
|
||||
|
||||
return array_values($links);
|
||||
}
|
||||
|
||||
private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse
|
||||
{
|
||||
$existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine]));
|
||||
$componentIndex = $this->indexLinksById($componentLinks);
|
||||
$keepIds = [];
|
||||
$pendingParents = [];
|
||||
$links = [];
|
||||
|
||||
foreach ($payload as $entry) {
|
||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink();
|
||||
if (!$linkId) {
|
||||
$linkId = $this->generateCuid();
|
||||
}
|
||||
if (!$link->getId()) {
|
||||
$link->setId($linkId);
|
||||
}
|
||||
|
||||
$pieceId = $this->resolveIdentifier($entry, ['pieceId']);
|
||||
if (!$pieceId) {
|
||||
return $this->json(['success' => false, 'error' => 'Pièce requise pour le squelette.'], 400);
|
||||
}
|
||||
$piece = $this->pieceRepository->find($pieceId);
|
||||
if (!$piece instanceof Piece) {
|
||||
return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404);
|
||||
}
|
||||
|
||||
$link->setMachine($machine);
|
||||
$link->setPiece($piece);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachinePieceRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->pieceRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachinePieceRequirement) {
|
||||
$link->setTypeMachinePieceRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $entry['overrides'] ?? null);
|
||||
|
||||
$pendingParents[$linkId] = $this->resolveIdentifier($entry, [
|
||||
'parentComponentLinkId',
|
||||
'parentLinkId',
|
||||
'parentMachineComponentLinkId',
|
||||
]);
|
||||
|
||||
$this->entityManager->persist($link);
|
||||
$links[$linkId] = $link;
|
||||
$keepIds[] = $linkId;
|
||||
}
|
||||
|
||||
foreach ($pendingParents as $linkId => $parentId) {
|
||||
if (!$parentId) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($links[$linkId])) {
|
||||
continue;
|
||||
}
|
||||
$parent = $componentIndex[$parentId] ?? null;
|
||||
if ($parent instanceof MachineComponentLink) {
|
||||
$links[$linkId]->setParentLink($parent);
|
||||
}
|
||||
}
|
||||
|
||||
$this->removeMissingLinks($existing, $keepIds);
|
||||
|
||||
return array_values($links);
|
||||
}
|
||||
|
||||
private function applyProductLinks(
|
||||
Machine $machine,
|
||||
array $payload,
|
||||
array $componentLinks,
|
||||
array $pieceLinks,
|
||||
): array|JsonResponse {
|
||||
$existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine]));
|
||||
$componentIndex = $this->indexLinksById($componentLinks);
|
||||
$pieceIndex = $this->indexLinksById($pieceLinks);
|
||||
$keepIds = [];
|
||||
$pendingParents = [];
|
||||
$links = [];
|
||||
|
||||
foreach ($payload as $entry) {
|
||||
$linkId = $this->resolveIdentifier($entry, ['id', 'linkId']);
|
||||
$link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink();
|
||||
if (!$linkId) {
|
||||
$linkId = $this->generateCuid();
|
||||
}
|
||||
if (!$link->getId()) {
|
||||
$link->setId($linkId);
|
||||
}
|
||||
|
||||
$productId = $this->resolveIdentifier($entry, ['productId']);
|
||||
if (!$productId) {
|
||||
return $this->json(['success' => false, 'error' => 'Produit requis pour le squelette.'], 400);
|
||||
}
|
||||
$product = $this->productRepository->find($productId);
|
||||
if (!$product instanceof Product) {
|
||||
return $this->json(['success' => false, 'error' => 'Produit introuvable.'], 404);
|
||||
}
|
||||
|
||||
$link->setMachine($machine);
|
||||
$link->setProduct($product);
|
||||
|
||||
$requirementId = $this->resolveIdentifier($entry, ['requirementId', 'typeMachineProductRequirementId']);
|
||||
if ($requirementId) {
|
||||
$requirement = $this->productRequirementRepository->find($requirementId);
|
||||
if ($requirement instanceof TypeMachineProductRequirement) {
|
||||
$link->setTypeMachineProductRequirement($requirement);
|
||||
}
|
||||
}
|
||||
|
||||
$pendingParents[$linkId] = [
|
||||
'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']),
|
||||
'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']),
|
||||
'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']),
|
||||
];
|
||||
|
||||
$this->entityManager->persist($link);
|
||||
$links[$linkId] = $link;
|
||||
$keepIds[] = $linkId;
|
||||
}
|
||||
|
||||
foreach ($pendingParents as $linkId => $parentIds) {
|
||||
if (!isset($links[$linkId])) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($parentIds['parentComponentLinkId']) && isset($componentIndex[$parentIds['parentComponentLinkId']])) {
|
||||
$links[$linkId]->setParentComponentLink($componentIndex[$parentIds['parentComponentLinkId']]);
|
||||
}
|
||||
if (!empty($parentIds['parentPieceLinkId']) && isset($pieceIndex[$parentIds['parentPieceLinkId']])) {
|
||||
$links[$linkId]->setParentPieceLink($pieceIndex[$parentIds['parentPieceLinkId']]);
|
||||
}
|
||||
if (!empty($parentIds['parentLinkId']) && isset($links[$parentIds['parentLinkId']])) {
|
||||
$links[$linkId]->setParentLink($links[$parentIds['parentLinkId']]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->removeMissingLinks($existing, $keepIds);
|
||||
|
||||
return array_values($links);
|
||||
}
|
||||
|
||||
private function normalizeMachineSkeletonResponse(
|
||||
Machine $machine,
|
||||
array $componentLinks,
|
||||
array $pieceLinks,
|
||||
array $productLinks,
|
||||
): array {
|
||||
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
||||
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
||||
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
||||
|
||||
// Build component hierarchy – track which IDs are children
|
||||
$childIds = [];
|
||||
foreach ($normalizedComponentLinks as $link) {
|
||||
$parentId = $link['parentComponentLinkId'] ?? null;
|
||||
if ($parentId && isset($componentIndex[$parentId])) {
|
||||
$componentIndex[$parentId]['childLinks'][] = $link;
|
||||
$childIds[$link['id']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add pieces to components recursively
|
||||
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
||||
|
||||
// Only return root-level components (exclude children already nested)
|
||||
$rootComponents = array_filter(
|
||||
$componentIndex,
|
||||
static fn (array $link) => !isset($childIds[$link['id']]),
|
||||
);
|
||||
|
||||
return [
|
||||
'machine' => $this->normalizeMachine($machine),
|
||||
'componentLinks' => array_values($rootComponents),
|
||||
'pieceLinks' => $normalizedPieceLinks,
|
||||
'productLinks' => $this->normalizeProductLinks($productLinks),
|
||||
];
|
||||
}
|
||||
|
||||
private function attachPiecesToComponents(array &$componentIndex, array $pieceLinks): void
|
||||
{
|
||||
foreach ($pieceLinks as $pieceLink) {
|
||||
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
||||
if ($parentId && isset($componentIndex[$parentId])) {
|
||||
$componentIndex[$parentId]['pieceLinks'][] = $pieceLink;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively attach to child components
|
||||
foreach ($componentIndex as &$component) {
|
||||
if (!empty($component['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function attachPiecesToChildComponents(array &$childLinks, array $pieceLinks): void
|
||||
{
|
||||
foreach ($childLinks as &$child) {
|
||||
$childId = $child['id'] ?? $child['linkId'] ?? null;
|
||||
if ($childId) {
|
||||
foreach ($pieceLinks as $pieceLink) {
|
||||
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
||||
if ($parentId === $childId) {
|
||||
$child['pieceLinks'][] = $pieceLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested children
|
||||
if (!empty($child['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeMachine(Machine $machine): array
|
||||
{
|
||||
$site = $machine->getSite();
|
||||
$typeMachine = $machine->getTypeMachine();
|
||||
|
||||
return [
|
||||
'id' => $machine->getId(),
|
||||
'name' => $machine->getName(),
|
||||
'reference' => $machine->getReference(),
|
||||
'prix' => $machine->getPrix(),
|
||||
'siteId' => $site->getId(),
|
||||
'site' => [
|
||||
'id' => $site->getId(),
|
||||
'name' => $site->getName(),
|
||||
],
|
||||
'typeMachineId' => $typeMachine?->getId(),
|
||||
'typeMachine' => $typeMachine ? [
|
||||
'id' => $typeMachine->getId(),
|
||||
'name' => $typeMachine->getName(),
|
||||
'category' => $typeMachine->getCategory(),
|
||||
'description' => $typeMachine->getDescription(),
|
||||
'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()),
|
||||
'componentRequirements' => $typeMachine->getComponentRequirements()
|
||||
->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req))
|
||||
->toArray(),
|
||||
'pieceRequirements' => $typeMachine->getPieceRequirements()
|
||||
->map(fn (TypeMachinePieceRequirement $req) => $this->normalizePieceRequirement($req))
|
||||
->toArray(),
|
||||
'productRequirements' => $typeMachine->getProductRequirements()
|
||||
->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req))
|
||||
->toArray(),
|
||||
] : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
||||
'documents' => null,
|
||||
'customFieldValues' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeCustomFields(Collection $customFields): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFields as $customField) {
|
||||
if (!$customField instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'id' => $customField->getId(),
|
||||
'name' => $customField->getName(),
|
||||
'type' => $customField->getType(),
|
||||
'required' => $customField->isRequired(),
|
||||
'options' => $customField->getOptions(),
|
||||
'defaultValue' => $customField->getDefaultValue(),
|
||||
'orderIndex' => $customField->getOrderIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeComponentLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineComponentLink $link): array {
|
||||
$composant = $link->getComposant();
|
||||
$requirement = $link->getTypeMachineComponentRequirement();
|
||||
$parentLink = $link->getParentLink();
|
||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'typeMachineComponentRequirementId' => $requirement?->getId(),
|
||||
'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function normalizePieceLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachinePieceLink $link): array {
|
||||
$piece = $link->getPiece();
|
||||
$requirement = $link->getTypeMachinePieceRequirement();
|
||||
$parentLink = $link->getParentLink();
|
||||
$parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'typeMachinePieceRequirementId' => $requirement?->getId(),
|
||||
'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null,
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'parentMachineComponentRequirementId' => $parentRequirementId,
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function normalizeProductLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineProductLink $link): array {
|
||||
$product = $link->getProduct();
|
||||
$requirement = $link->getTypeMachineProductRequirement();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'productId' => $product->getId(),
|
||||
'product' => $this->normalizeProduct($product),
|
||||
'typeMachineProductRequirementId' => $requirement?->getId(),
|
||||
'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null,
|
||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function normalizeComposant(Composant $composant): array
|
||||
{
|
||||
return [
|
||||
'id' => $composant->getId(),
|
||||
'name' => $composant->getName(),
|
||||
'reference' => $composant->getReference(),
|
||||
'prix' => $composant->getPrix(),
|
||||
'typeComposantId' => $composant->getTypeComposant()?->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()),
|
||||
'productId' => $composant->getProduct()?->getId(),
|
||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePiece(Piece $piece): array
|
||||
{
|
||||
return [
|
||||
'id' => $piece->getId(),
|
||||
'name' => $piece->getName(),
|
||||
'reference' => $piece->getReference(),
|
||||
'prix' => $piece->getPrix(),
|
||||
'typePieceId' => $piece->getTypePiece()?->getId(),
|
||||
'typePiece' => $this->normalizeModelType($piece->getTypePiece()),
|
||||
'productId' => $piece->getProduct()?->getId(),
|
||||
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeProduct(Product $product): array
|
||||
{
|
||||
return [
|
||||
'id' => $product->getId(),
|
||||
'name' => $product->getName(),
|
||||
'reference' => $product->getReference(),
|
||||
'supplierPrice' => $product->getSupplierPrice(),
|
||||
'typeProductId' => $product->getTypeProduct()?->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($product->getTypeProduct()),
|
||||
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||
'documents' => [],
|
||||
'customFields' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeModelType(?ModelType $type): ?array
|
||||
{
|
||||
if (!$type instanceof ModelType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $type->getId(),
|
||||
'name' => $type->getName(),
|
||||
'code' => $type->getCode(),
|
||||
'category' => $type->getCategory()->value,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeComponentRequirement(TypeMachineComponentRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typeComposantId' => $requirement->getTypeComposant()->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePieceRequirement(TypeMachinePieceRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typePieceId' => $requirement->getTypePiece()->getId(),
|
||||
'typePiece' => $this->normalizeModelType($requirement->getTypePiece()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeProductRequirement(TypeMachineProductRequirement $requirement): array
|
||||
{
|
||||
return [
|
||||
'id' => $requirement->getId(),
|
||||
'label' => $requirement->getLabel(),
|
||||
'minCount' => $requirement->getMinCount(),
|
||||
'maxCount' => $requirement->getMaxCount(),
|
||||
'required' => $requirement->isRequired(),
|
||||
'typeProductId' => $requirement->getTypeProduct()->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeConstructeurs(Collection $constructeurs): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($constructeurs as $constructeur) {
|
||||
$items[] = [
|
||||
'id' => $constructeur->getId(),
|
||||
'name' => $constructeur->getName(),
|
||||
'email' => $constructeur->getEmail(),
|
||||
'phone' => $constructeur->getPhone(),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeOverrides(object $link): ?array
|
||||
{
|
||||
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
||||
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
||||
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
||||
|
||||
if (null === $name && null === $reference && null === $prix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'reference' => $reference,
|
||||
'prix' => $prix,
|
||||
];
|
||||
}
|
||||
|
||||
private function applyOverrides(object $link, mixed $overrides): void
|
||||
{
|
||||
if (!is_array($overrides)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $overrides) && method_exists($link, 'setNameOverride')) {
|
||||
$link->setNameOverride($this->stringOrNull($overrides['name']));
|
||||
}
|
||||
if (array_key_exists('reference', $overrides) && method_exists($link, 'setReferenceOverride')) {
|
||||
$link->setReferenceOverride($this->stringOrNull($overrides['reference']));
|
||||
}
|
||||
if (array_key_exists('prix', $overrides) && method_exists($link, 'setPrixOverride')) {
|
||||
$link->setPrixOverride($this->stringOrNull($overrides['prix']));
|
||||
}
|
||||
}
|
||||
|
||||
private function stringOrNull(mixed $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
$string = trim((string) $value);
|
||||
|
||||
return '' === $string ? null : $string;
|
||||
}
|
||||
|
||||
private function resolveIdentifier(array $entry, array $keys): ?string
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $entry)) {
|
||||
continue;
|
||||
}
|
||||
$value = $entry[$key];
|
||||
if (null === $value || '' === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, object> $links
|
||||
*
|
||||
* @return array<string, object>
|
||||
*/
|
||||
private function indexLinksById(array $links): array
|
||||
{
|
||||
$indexed = [];
|
||||
foreach ($links as $link) {
|
||||
if (method_exists($link, 'getId') && $link->getId()) {
|
||||
$indexed[$link->getId()] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
return $indexed;
|
||||
}
|
||||
|
||||
private function indexNormalizedLinks(array $links): array
|
||||
{
|
||||
$indexed = [];
|
||||
foreach ($links as $link) {
|
||||
if (is_array($link) && isset($link['id'])) {
|
||||
$indexed[$link['id']] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
return $indexed;
|
||||
}
|
||||
|
||||
private function removeMissingLinks(array $existing, array $keepIds): void
|
||||
{
|
||||
$keep = array_flip($keepIds);
|
||||
foreach ($existing as $link) {
|
||||
if (!method_exists($link, 'getId')) {
|
||||
continue;
|
||||
}
|
||||
$id = $link->getId();
|
||||
if ($id && !isset($keep[$id])) {
|
||||
$this->entityManager->remove($link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
55
src/Controller/ModelTypeConversionController.php
Normal file
55
src/Controller/ModelTypeConversionController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\ModelTypeRepository;
|
||||
use App\Service\ModelTypeCategoryConversionService;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class ModelTypeConversionController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ModelTypeRepository $modelTypes,
|
||||
private readonly ModelTypeCategoryConversionService $conversionService,
|
||||
) {}
|
||||
|
||||
#[Route('/api/model_types/{id}/conversion-check', name: 'api_model_type_conversion_check', methods: ['GET'])]
|
||||
public function check(string $id): JsonResponse
|
||||
{
|
||||
$modelType = $this->modelTypes->find($id);
|
||||
|
||||
if (!$modelType) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Catégorie introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse($this->conversionService->checkConversion($id));
|
||||
}
|
||||
|
||||
#[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])]
|
||||
public function convert(string $id): JsonResponse
|
||||
{
|
||||
$modelType = $this->modelTypes->find($id);
|
||||
|
||||
if (!$modelType) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Catégorie introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$result = $this->conversionService->convert($id);
|
||||
|
||||
if (!$result['success']) {
|
||||
return new JsonResponse($result, Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
}
|
||||
79
src/Controller/PieceHistoryController.php
Normal file
79
src/Controller/PieceHistoryController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class PieceHistoryController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PieceRepository $pieces,
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
|
||||
public function __invoke(string $id): JsonResponse
|
||||
{
|
||||
$piece = $this->pieces->find($id);
|
||||
if (!$piece) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Pièce introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$logs = $this->auditLogs->findEntityHistory('piece', $id, 200);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$logs,
|
||||
))));
|
||||
|
||||
$actorMap = [];
|
||||
if ([] !== $actorIds) {
|
||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||
foreach ($profiles as $profile) {
|
||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||
if ('' === $label) {
|
||||
$label = $profile->getEmail() ?? $profile->getId();
|
||||
}
|
||||
$actorMap[$profile->getId()] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static function ($log) use ($actorMap) {
|
||||
$actorId = $log->getActorProfileId();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $log->getSnapshot(),
|
||||
];
|
||||
},
|
||||
$logs,
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => count($items),
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
src/Controller/ProductHistoryController.php
Normal file
79
src/Controller/ProductHistoryController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class ProductHistoryController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductRepository $products,
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
|
||||
public function __invoke(string $id): JsonResponse
|
||||
{
|
||||
$product = $this->products->find($id);
|
||||
if (!$product) {
|
||||
return new JsonResponse(
|
||||
['message' => 'Produit introuvable.'],
|
||||
Response::HTTP_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
$logs = $this->auditLogs->findEntityHistory('product', $id, 200);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$logs,
|
||||
))));
|
||||
|
||||
$actorMap = [];
|
||||
if ([] !== $actorIds) {
|
||||
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||
foreach ($profiles as $profile) {
|
||||
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||
if ('' === $label) {
|
||||
$label = $profile->getEmail() ?? $profile->getId();
|
||||
}
|
||||
$actorMap[$profile->getId()] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static function ($log) use ($actorMap) {
|
||||
$actorId = $log->getActorProfileId();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $log->getSnapshot(),
|
||||
];
|
||||
},
|
||||
$logs,
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => count($items),
|
||||
]);
|
||||
}
|
||||
}
|
||||
89
src/Controller/SessionProfileController.php
Normal file
89
src/Controller/SessionProfileController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\ProfileRepository;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class SessionProfileController
|
||||
{
|
||||
public function __construct(private readonly ProfileRepository $profiles) {}
|
||||
|
||||
#[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])]
|
||||
public function getActiveProfile(Request $request): JsonResponse
|
||||
{
|
||||
$session = $request->getSession();
|
||||
if (!$session instanceof SessionInterface) {
|
||||
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$profileId = $session->get('profileId');
|
||||
if (!$profileId) {
|
||||
return new JsonResponse(['message' => 'Aucun profil actif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$profile = $this->profiles->find($profileId);
|
||||
if (!$profile || !$profile->isActive()) {
|
||||
$session->remove('profileId');
|
||||
|
||||
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'id' => $profile->getId(),
|
||||
'firstName' => $profile->getFirstName(),
|
||||
'lastName' => $profile->getLastName(),
|
||||
'email' => $profile->getEmail(),
|
||||
'isActive' => $profile->isActive(),
|
||||
'roles' => $profile->getRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/session/profile', name: 'api_session_profile_post', methods: ['POST'])]
|
||||
public function activateProfile(Request $request): JsonResponse
|
||||
{
|
||||
$session = $request->getSession();
|
||||
if (!$session instanceof SessionInterface) {
|
||||
return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$payload = $request->toArray();
|
||||
$profileId = $payload['profileId'] ?? null;
|
||||
|
||||
if (!$profileId) {
|
||||
return new JsonResponse(['message' => 'profileId est requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$profile = $this->profiles->find($profileId);
|
||||
if (!$profile || !$profile->isActive()) {
|
||||
return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$session->set('profileId', $profile->getId());
|
||||
|
||||
return new JsonResponse([
|
||||
'id' => $profile->getId(),
|
||||
'firstName' => $profile->getFirstName(),
|
||||
'lastName' => $profile->getLastName(),
|
||||
'email' => $profile->getEmail(),
|
||||
'isActive' => $profile->isActive(),
|
||||
'roles' => $profile->getRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/session/profile', name: 'api_session_profile_delete', methods: ['DELETE'])]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof SessionInterface) {
|
||||
$session->invalidate();
|
||||
}
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
}
|
||||
80
src/Controller/SessionProfilesController.php
Normal file
80
src/Controller/SessionProfilesController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Profile;
|
||||
use App\Repository\ProfileRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class SessionProfilesController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProfileRepository $profiles,
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
) {}
|
||||
|
||||
#[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])]
|
||||
public function list(): JsonResponse
|
||||
{
|
||||
$items = $this->profiles->createQueryBuilder('p')
|
||||
->andWhere('p.isActive = :active')
|
||||
->setParameter('active', true)
|
||||
->orderBy('p.firstName', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
return new JsonResponse(array_map([$this, 'serializeProfile'], $items));
|
||||
}
|
||||
|
||||
#[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])]
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->toArray();
|
||||
$firstName = trim((string) ($payload['firstName'] ?? ''));
|
||||
$lastName = trim((string) ($payload['lastName'] ?? ''));
|
||||
|
||||
if ('' === $firstName || '' === $lastName) {
|
||||
return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
$profile->setFirstName($firstName);
|
||||
$profile->setLastName($lastName);
|
||||
$profile->setIsActive(true);
|
||||
|
||||
$this->entityManager->persist($profile);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse($this->serializeProfile($profile), JsonResponse::HTTP_CREATED);
|
||||
}
|
||||
|
||||
#[Route('/api/session/profiles/{id}', name: 'api_session_profiles_delete', methods: ['DELETE'])]
|
||||
public function delete(string $id): JsonResponse
|
||||
{
|
||||
$profile = $this->profiles->find($id);
|
||||
if (!$profile) {
|
||||
return new JsonResponse(['message' => 'Profil introuvable.'], JsonResponse::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$profile->setIsActive(false);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
private function serializeProfile(Profile $profile): array
|
||||
{
|
||||
return [
|
||||
'id' => $profile->getId(),
|
||||
'firstName' => $profile->getFirstName(),
|
||||
'lastName' => $profile->getLastName(),
|
||||
'isActive' => $profile->isActive(),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
src/Controller/TestController.php
Normal file
18
src/Controller/TestController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class TestController extends AbstractController
|
||||
{
|
||||
#[Route('/api/test', name: 'api_test', methods: ['GET', 'POST'])]
|
||||
public function test(): JsonResponse
|
||||
{
|
||||
return $this->json(['status' => 'ok', 'message' => 'Test endpoint works!']);
|
||||
}
|
||||
}
|
||||
111
src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php
Normal file
111
src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine\QuoteStrategy;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\ORM\Internal\SQLResultCasing;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||
use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
|
||||
use Doctrine\ORM\Mapping\QuoteStrategy;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function explode;
|
||||
use function implode;
|
||||
|
||||
/**
|
||||
* Quote all identifiers to preserve camelCase column names in Postgres.
|
||||
*/
|
||||
final class AlwaysQuoteStrategy implements QuoteStrategy
|
||||
{
|
||||
use SQLResultCasing;
|
||||
|
||||
public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string
|
||||
{
|
||||
return $platform->quoteSingleIdentifier($class->fieldMappings[$fieldName]->columnName);
|
||||
}
|
||||
|
||||
public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
|
||||
{
|
||||
$tableName = $platform->quoteSingleIdentifier($class->table['name']);
|
||||
|
||||
if (!empty($class->table['schema'])) {
|
||||
return $platform->quoteSingleIdentifier($class->table['schema']).'.'.$tableName;
|
||||
}
|
||||
|
||||
return $tableName;
|
||||
}
|
||||
|
||||
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
|
||||
{
|
||||
return implode('.', array_map(
|
||||
static fn (string $part) => $platform->quoteSingleIdentifier($part),
|
||||
explode('.', $definition['sequenceName']),
|
||||
));
|
||||
}
|
||||
|
||||
public function getJoinTableName(
|
||||
ManyToManyOwningSideMapping $association,
|
||||
ClassMetadata $class,
|
||||
AbstractPlatform $platform,
|
||||
): string {
|
||||
$schema = '';
|
||||
|
||||
if (isset($association->joinTable->schema)) {
|
||||
$schema = $platform->quoteSingleIdentifier($association->joinTable->schema).'.';
|
||||
}
|
||||
|
||||
return $schema.$platform->quoteSingleIdentifier($association->joinTable->name);
|
||||
}
|
||||
|
||||
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
|
||||
{
|
||||
return $platform->quoteSingleIdentifier($joinColumn->name);
|
||||
}
|
||||
|
||||
public function getReferencedJoinColumnName(
|
||||
JoinColumnMapping $joinColumn,
|
||||
ClassMetadata $class,
|
||||
AbstractPlatform $platform,
|
||||
): string {
|
||||
return $platform->quoteSingleIdentifier($joinColumn->referencedColumnName);
|
||||
}
|
||||
|
||||
public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
|
||||
{
|
||||
$quotedColumnNames = [];
|
||||
|
||||
foreach ($class->identifier as $fieldName) {
|
||||
if (isset($class->fieldMappings[$fieldName])) {
|
||||
$quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$assoc = $class->associationMappings[$fieldName];
|
||||
assert($assoc->isToOneOwningSide());
|
||||
$joinColumns = $assoc->joinColumns;
|
||||
$assocQuotedColumnNames = array_map(
|
||||
static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name),
|
||||
$joinColumns,
|
||||
);
|
||||
|
||||
$quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames);
|
||||
}
|
||||
|
||||
return $quotedColumnNames;
|
||||
}
|
||||
|
||||
public function getColumnAlias(
|
||||
string $columnName,
|
||||
int $counter,
|
||||
AbstractPlatform $platform,
|
||||
?ClassMetadata $class = null,
|
||||
): string {
|
||||
return $this->getSQLResultCasing($platform, $columnName.'_'.$counter);
|
||||
}
|
||||
}
|
||||
117
src/Entity/AuditLog.php
Normal file
117
src/Entity/AuditLog.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AuditLogRepository::class)]
|
||||
#[ORM\Table(name: 'audit_logs')]
|
||||
#[ORM\Index(name: 'idx_audit_entity', columns: ['entityType', 'entityId'])]
|
||||
#[ORM\Index(name: 'idx_audit_created_at', columns: ['createdAt'])]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
class AuditLog
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 50)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private string $entityId;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 20)]
|
||||
private string $action;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
private ?array $diff = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
private ?array $snapshot = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 36, nullable: true)]
|
||||
private ?string $actorProfileId = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct(
|
||||
string $entityType,
|
||||
string $entityId,
|
||||
string $action,
|
||||
?array $diff = null,
|
||||
?array $snapshot = null,
|
||||
?string $actorProfileId = null,
|
||||
) {
|
||||
$this->entityType = $entityType;
|
||||
$this->entityId = $entityId;
|
||||
$this->action = $action;
|
||||
$this->diff = $diff;
|
||||
$this->snapshot = $snapshot;
|
||||
$this->actorProfileId = $actorProfileId;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function initializeAuditLog(): void
|
||||
{
|
||||
if (!isset($this->createdAt)) {
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public function getEntityId(): string
|
||||
{
|
||||
return $this->entityId;
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function getDiff(): ?array
|
||||
{
|
||||
return $this->diff;
|
||||
}
|
||||
|
||||
public function getSnapshot(): ?array
|
||||
{
|
||||
return $this->snapshot;
|
||||
}
|
||||
|
||||
public function getActorProfileId(): ?string
|
||||
{
|
||||
return $this->actorProfileId;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
// Keep the same lightweight CUID-like strategy used across the project.
|
||||
return 'cl'.substr(strtolower(base_convert(bin2hex(random_bytes(12)), 16, 36)), 0, 24);
|
||||
}
|
||||
}
|
||||
282
src/Entity/Composant.php
Normal file
282
src/Entity/Composant.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\ComposantRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ComposantRepository::class)]
|
||||
#[ORM\Table(name: 'composants')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['composant:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Composant
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['composant:read', 'document:list'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Groups(['composant:read', 'document:list'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?string $reference = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?string $prix = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?array $structure = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'composants')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true)]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?ModelType $typeComposant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'composants')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['composant:read'])]
|
||||
private ?Product $product = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'composants')]
|
||||
#[ORM\JoinTable(
|
||||
name: '_ComposantConstructeurs',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
)]
|
||||
#[Groups(['composant:read'])]
|
||||
private Collection $constructeurs;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Document>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: Document::class)]
|
||||
#[Groups(['composant:read'])]
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: CustomFieldValue::class)]
|
||||
#[Groups(['composant:read'])]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineComponentLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['composant:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['composant:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReference(): ?string
|
||||
{
|
||||
return $this->reference;
|
||||
}
|
||||
|
||||
public function setReference(?string $reference): static
|
||||
{
|
||||
$this->reference = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrix(): ?string
|
||||
{
|
||||
return $this->prix;
|
||||
}
|
||||
|
||||
public function setPrix(?string $prix): static
|
||||
{
|
||||
$this->prix = $prix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
return $this->structure;
|
||||
}
|
||||
|
||||
public function setStructure(?array $structure): static
|
||||
{
|
||||
$this->structure = $structure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ?ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(?ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Constructeur>
|
||||
*/
|
||||
public function getConstructeurs(): Collection
|
||||
{
|
||||
return $this->constructeurs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<Constructeur> $constructeurs
|
||||
*/
|
||||
public function setConstructeurs(iterable $constructeurs): static
|
||||
{
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
|
||||
foreach ($constructeurs as $constructeur) {
|
||||
if ($constructeur instanceof Constructeur && !$this->constructeurs->contains($constructeur)) {
|
||||
$this->constructeurs->add($constructeur);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
if (!$this->constructeurs->contains($constructeur)) {
|
||||
$this->constructeurs->add($constructeur);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
$this->constructeurs->removeElement($constructeur);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Document>
|
||||
*/
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomFieldValue>
|
||||
*/
|
||||
public function getCustomFieldValues(): Collection
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
155
src/Entity/Constructeur.php
Normal file
155
src/Entity/Constructeur.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\ConstructeurRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
||||
#[ORM\Table(name: 'constructeurs')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Constructeur
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
private ?string $phone = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Machine>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Machine::class, mappedBy: 'constructeurs')]
|
||||
private Collection $machines;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Composant>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Composant::class, mappedBy: 'constructeurs')]
|
||||
private Collection $composants;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Piece>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Piece::class, mappedBy: 'constructeurs')]
|
||||
private Collection $pieces;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Product>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Product::class, mappedBy: 'constructeurs')]
|
||||
private Collection $products;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machines = new ArrayCollection();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): ?string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(?string $phone): static
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
211
src/Entity/CustomField.php
Normal file
211
src/Entity/CustomField.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\CustomFieldRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
|
||||
#[ORM\Table(name: 'custom_fields')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class CustomField
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 50)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private string $type;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private bool $required = false;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'defaultValue')]
|
||||
private ?string $defaultValue = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private ?array $options = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'customFields')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?TypeMachine $typeMachine = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'customFields')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?ModelType $typeComposant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceCustomFields')]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?ModelType $typePiece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productCustomFields')]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?ModelType $typeProduct = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'customField', targetEntity: CustomFieldValue::class)]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefaultValue(): ?string
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
public function setDefaultValue(?string $defaultValue): static
|
||||
{
|
||||
$this->defaultValue = $defaultValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOptions(): ?array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function setOptions(?array $options): static
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): ?TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(?TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
174
src/Entity/CustomFieldValue.php
Normal file
174
src/Entity/CustomFieldValue.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\CustomFieldValueRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: CustomFieldValueRepository::class)]
|
||||
#[ORM\Table(name: 'custom_field_values')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class CustomFieldValue
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private string $value;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: CustomField::class, inversedBy: 'customFieldValues')]
|
||||
#[ORM\JoinColumn(name: 'customFieldId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private CustomField $customField;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'customFieldValues')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Machine $machine = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'customFieldValues')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Composant $composant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'customFieldValues')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Piece $piece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'customFieldValues')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomField(): CustomField
|
||||
{
|
||||
return $this->customField;
|
||||
}
|
||||
|
||||
public function setCustomField(CustomField $customField): static
|
||||
{
|
||||
$this->customField = $customField;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachine(): ?Machine
|
||||
{
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setMachine(?Machine $machine): static
|
||||
{
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComposant(): ?Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(?Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPiece(): ?Piece
|
||||
{
|
||||
return $this->piece;
|
||||
}
|
||||
|
||||
public function setPiece(?Piece $piece): static
|
||||
{
|
||||
$this->piece = $piece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
256
src/Entity/Document.php
Normal file
256
src/Entity/Document.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\DocumentRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
||||
#[ORM\Table(name: 'documents')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(normalizationContext: ['groups' => ['document:list']]),
|
||||
new Get(normalizationContext: ['groups' => ['document:list', 'document:detail']]),
|
||||
new Post(),
|
||||
new Put(),
|
||||
new Delete(),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Document
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private string $filename;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[Groups(['document:detail', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private string $path;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
|
||||
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private string $mimeType;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
#[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
|
||||
private int $size;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['document:list'])]
|
||||
private ?Machine $machine = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['document:list'])]
|
||||
private ?Composant $composant = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['document:list'])]
|
||||
private ?Piece $piece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['document:list'])]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
|
||||
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
#[Groups(['document:list'])]
|
||||
private ?Site $site = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['document:list'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function setFilename(string $filename): static
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function setPath(string $path): static
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
public function setMimeType(string $mimeType): static
|
||||
{
|
||||
$this->mimeType = $mimeType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function setSize(int $size): static
|
||||
{
|
||||
$this->size = $size;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachine(): ?Machine
|
||||
{
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setMachine(?Machine $machine): static
|
||||
{
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComposant(): ?Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(?Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPiece(): ?Piece
|
||||
{
|
||||
return $this->piece;
|
||||
}
|
||||
|
||||
public function setPiece(?Piece $piece): static
|
||||
{
|
||||
$this->piece = $piece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSite(): ?Site
|
||||
{
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
public function setSite(?Site $site): static
|
||||
{
|
||||
$this->site = $site;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
254
src/Entity/Machine.php
Normal file
254
src/Entity/Machine.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\MachineRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MachineRepository::class)]
|
||||
#[ORM\Table(name: 'machines')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class Machine
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['document:list'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Groups(['document:list'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
private ?string $reference = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||
private ?string $prix = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'machines')]
|
||||
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Site $site;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'machines')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: true)]
|
||||
private ?TypeMachine $typeMachine = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'machines')]
|
||||
#[ORM\JoinTable(
|
||||
name: '_MachineConstructeurs',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
)]
|
||||
private Collection $constructeurs;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineComponentLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $componentLinks;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $pieceLinks;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineProductLink::class)]
|
||||
private Collection $productLinks;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Document>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: Document::class)]
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: CustomFieldValue::class)]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
$this->componentLinks = new ArrayCollection();
|
||||
$this->pieceLinks = new ArrayCollection();
|
||||
$this->productLinks = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReference(): ?string
|
||||
{
|
||||
return $this->reference;
|
||||
}
|
||||
|
||||
public function setReference(?string $reference): static
|
||||
{
|
||||
$this->reference = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrix(): ?string
|
||||
{
|
||||
return $this->prix;
|
||||
}
|
||||
|
||||
public function setPrix(?string $prix): static
|
||||
{
|
||||
$this->prix = $prix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSite(): Site
|
||||
{
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
public function setSite(Site $site): static
|
||||
{
|
||||
$this->site = $site;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): ?TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(?TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Constructeur>
|
||||
*/
|
||||
public function getConstructeurs(): Collection
|
||||
{
|
||||
return $this->constructeurs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MachineComponentLink>
|
||||
*/
|
||||
public function getComponentLinks(): Collection
|
||||
{
|
||||
return $this->componentLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MachinePieceLink>
|
||||
*/
|
||||
public function getPieceLinks(): Collection
|
||||
{
|
||||
return $this->pieceLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MachineProductLink>
|
||||
*/
|
||||
public function getProductLinks(): Collection
|
||||
{
|
||||
return $this->productLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Document>
|
||||
*/
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomFieldValue>
|
||||
*/
|
||||
public function getCustomFieldValues(): Collection
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
199
src/Entity/MachineComponentLink.php
Normal file
199
src/Entity/MachineComponentLink.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MachineComponentLinkRepository::class)]
|
||||
#[ORM\Table(name: 'machine_component_links')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class MachineComponentLink
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'componentLinks')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Composant $composant;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'childLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineComponentLink $parentLink = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineComponentLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $childLinks;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachineComponentRequirement::class, inversedBy: 'machineComponentLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineComponentRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachineComponentRequirement $typeMachineComponentRequirement = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $pieceLinks;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'parentComponentLink', targetEntity: MachineProductLink::class)]
|
||||
private Collection $productLinks;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'nameOverride')]
|
||||
private ?string $nameOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'referenceOverride')]
|
||||
private ?string $referenceOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'prixOverride')]
|
||||
private ?string $prixOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->childLinks = new ArrayCollection();
|
||||
$this->pieceLinks = new ArrayCollection();
|
||||
$this->productLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachine(): Machine
|
||||
{
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setMachine(Machine $machine): static
|
||||
{
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComposant(): Composant
|
||||
{
|
||||
return $this->composant;
|
||||
}
|
||||
|
||||
public function setComposant(Composant $composant): static
|
||||
{
|
||||
$this->composant = $composant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineComponentLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
}
|
||||
|
||||
public function setParentLink(?MachineComponentLink $parentLink): static
|
||||
{
|
||||
$this->parentLink = $parentLink;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachineComponentRequirement(): ?TypeMachineComponentRequirement
|
||||
{
|
||||
return $this->typeMachineComponentRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachineComponentRequirement(?TypeMachineComponentRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachineComponentRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNameOverride(): ?string
|
||||
{
|
||||
return $this->nameOverride;
|
||||
}
|
||||
|
||||
public function setNameOverride(?string $nameOverride): static
|
||||
{
|
||||
$this->nameOverride = $nameOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReferenceOverride(): ?string
|
||||
{
|
||||
return $this->referenceOverride;
|
||||
}
|
||||
|
||||
public function setReferenceOverride(?string $referenceOverride): static
|
||||
{
|
||||
$this->referenceOverride = $referenceOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrixOverride(): ?string
|
||||
{
|
||||
return $this->prixOverride;
|
||||
}
|
||||
|
||||
public function setPrixOverride(?string $prixOverride): static
|
||||
{
|
||||
$this->prixOverride = $prixOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
185
src/Entity/MachinePieceLink.php
Normal file
185
src/Entity/MachinePieceLink.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MachinePieceLinkRepository::class)]
|
||||
#[ORM\Table(name: 'machine_piece_links')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class MachinePieceLink
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'pieceLinks')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Piece $piece;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'pieceLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineComponentLink $parentLink = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachinePieceRequirement::class, inversedBy: 'machinePieceLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachinePieceRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachinePieceRequirement $typeMachinePieceRequirement = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'parentPieceLink', targetEntity: MachineProductLink::class)]
|
||||
private Collection $productLinks;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'nameOverride')]
|
||||
private ?string $nameOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'referenceOverride')]
|
||||
private ?string $referenceOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'prixOverride')]
|
||||
private ?string $prixOverride = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->productLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachine(): Machine
|
||||
{
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setMachine(Machine $machine): static
|
||||
{
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPiece(): Piece
|
||||
{
|
||||
return $this->piece;
|
||||
}
|
||||
|
||||
public function setPiece(Piece $piece): static
|
||||
{
|
||||
$this->piece = $piece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineComponentLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
}
|
||||
|
||||
public function setParentLink(?MachineComponentLink $parentLink): static
|
||||
{
|
||||
$this->parentLink = $parentLink;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachinePieceRequirement(): ?TypeMachinePieceRequirement
|
||||
{
|
||||
return $this->typeMachinePieceRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachinePieceRequirement(?TypeMachinePieceRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachinePieceRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNameOverride(): ?string
|
||||
{
|
||||
return $this->nameOverride;
|
||||
}
|
||||
|
||||
public function setNameOverride(?string $nameOverride): static
|
||||
{
|
||||
$this->nameOverride = $nameOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReferenceOverride(): ?string
|
||||
{
|
||||
return $this->referenceOverride;
|
||||
}
|
||||
|
||||
public function setReferenceOverride(?string $referenceOverride): static
|
||||
{
|
||||
$this->referenceOverride = $referenceOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrixOverride(): ?string
|
||||
{
|
||||
return $this->prixOverride;
|
||||
}
|
||||
|
||||
public function setPrixOverride(?string $prixOverride): static
|
||||
{
|
||||
$this->prixOverride = $prixOverride;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
172
src/Entity/MachineProductLink.php
Normal file
172
src/Entity/MachineProductLink.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\MachineProductLinkRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: MachineProductLinkRepository::class)]
|
||||
#[ORM\Table(name: 'machine_product_links')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class MachineProductLink
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'productLinks')]
|
||||
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Machine $machine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'machineLinks')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Product $product;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachineProductRequirement::class, inversedBy: 'machineProductLinks')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineProductRequirementId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?TypeMachineProductRequirement $typeMachineProductRequirement = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineProductLink::class, inversedBy: 'childLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineProductLink $parentLink = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'parentLink', targetEntity: MachineProductLink::class)]
|
||||
private Collection $childLinks;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'productLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentComponentLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachineComponentLink $parentComponentLink = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: MachinePieceLink::class, inversedBy: 'productLinks')]
|
||||
#[ORM\JoinColumn(name: 'parentPieceLinkId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?MachinePieceLink $parentPieceLink = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->childLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachine(): Machine
|
||||
{
|
||||
return $this->machine;
|
||||
}
|
||||
|
||||
public function setMachine(Machine $machine): static
|
||||
{
|
||||
$this->machine = $machine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachineProductRequirement(): ?TypeMachineProductRequirement
|
||||
{
|
||||
return $this->typeMachineProductRequirement;
|
||||
}
|
||||
|
||||
public function setTypeMachineProductRequirement(?TypeMachineProductRequirement $requirement): static
|
||||
{
|
||||
$this->typeMachineProductRequirement = $requirement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentLink(): ?MachineProductLink
|
||||
{
|
||||
return $this->parentLink;
|
||||
}
|
||||
|
||||
public function setParentLink(?MachineProductLink $parentLink): static
|
||||
{
|
||||
$this->parentLink = $parentLink;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentComponentLink(): ?MachineComponentLink
|
||||
{
|
||||
return $this->parentComponentLink;
|
||||
}
|
||||
|
||||
public function setParentComponentLink(?MachineComponentLink $parentComponentLink): static
|
||||
{
|
||||
$this->parentComponentLink = $parentComponentLink;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentPieceLink(): ?MachinePieceLink
|
||||
{
|
||||
return $this->parentPieceLink;
|
||||
}
|
||||
|
||||
public function setParentPieceLink(?MachinePieceLink $parentPieceLink): static
|
||||
{
|
||||
$this->parentPieceLink = $parentPieceLink;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
338
src/Entity/ModelType.php
Normal file
338
src/Entity/ModelType.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Enum\ModelCategory;
|
||||
use App\Repository\ModelTypeRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ModelTypeRepository::class)]
|
||||
#[ORM\Table(name: 'model_types')]
|
||||
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class ModelType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read', 'model_type:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 120)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 60, unique: true)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private string $code;
|
||||
|
||||
#[ORM\Column(enumType: ModelCategory::class)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private ModelCategory $category;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private ?string $notes = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'componentSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private ?array $componentSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'pieceSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private ?array $pieceSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productSkeleton')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private ?array $productSkeleton = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['model_type:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
private ?array $pendingStructure = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Composant>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: Composant::class)]
|
||||
private Collection $composants;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Piece>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: Piece::class)]
|
||||
private Collection $pieces;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Product>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: Product::class)]
|
||||
private Collection $products;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: TypeMachineComponentRequirement::class)]
|
||||
private Collection $componentRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: TypeMachinePieceRequirement::class)]
|
||||
private Collection $pieceRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: TypeMachineProductRequirement::class)]
|
||||
private Collection $productRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: CustomField::class)]
|
||||
private Collection $customFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: CustomField::class)]
|
||||
private Collection $pieceCustomFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: CustomField::class)]
|
||||
private Collection $productCustomFields;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->products = new ArrayCollection();
|
||||
$this->componentRequirements = new ArrayCollection();
|
||||
$this->pieceRequirements = new ArrayCollection();
|
||||
$this->productRequirements = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->pieceCustomFields = new ArrayCollection();
|
||||
$this->productCustomFields = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): static
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategory(): ModelCategory
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
public function setCategory(ModelCategory $category): static
|
||||
{
|
||||
$this->category = $category;
|
||||
|
||||
if (null !== $this->pendingStructure) {
|
||||
$this->applyStructureForCategory($this->pendingStructure, $category);
|
||||
$this->pendingStructure = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNotes(): ?string
|
||||
{
|
||||
return $this->notes;
|
||||
}
|
||||
|
||||
public function setNotes(?string $notes): static
|
||||
{
|
||||
$this->notes = $notes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComponentSkeleton(): ?array
|
||||
{
|
||||
return $this->componentSkeleton;
|
||||
}
|
||||
|
||||
public function setComponentSkeleton(?array $componentSkeleton): static
|
||||
{
|
||||
$this->componentSkeleton = $componentSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPieceSkeleton(): ?array
|
||||
{
|
||||
return $this->pieceSkeleton;
|
||||
}
|
||||
|
||||
public function setPieceSkeleton(?array $pieceSkeleton): static
|
||||
{
|
||||
$this->pieceSkeleton = $pieceSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProductSkeleton(): ?array
|
||||
{
|
||||
return $this->productSkeleton;
|
||||
}
|
||||
|
||||
public function setProductSkeleton(?array $productSkeleton): static
|
||||
{
|
||||
$this->productSkeleton = $productSkeleton;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['model_type:read'])]
|
||||
public function getStructure(): ?array
|
||||
{
|
||||
return match ($this->category) {
|
||||
ModelCategory::COMPONENT => $this->componentSkeleton,
|
||||
ModelCategory::PIECE => $this->pieceSkeleton,
|
||||
ModelCategory::PRODUCT => $this->productSkeleton,
|
||||
};
|
||||
}
|
||||
|
||||
#[Groups(['model_type:write'])]
|
||||
public function setStructure(?array $structure): static
|
||||
{
|
||||
if (!isset($this->category)) {
|
||||
$this->pendingStructure = $structure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->applyStructureForCategory($structure, $this->category);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
|
||||
private function applyStructureForCategory(?array $structure, ModelCategory $category): void
|
||||
{
|
||||
if (ModelCategory::COMPONENT === $category) {
|
||||
$this->componentSkeleton = $structure;
|
||||
$this->pieceSkeleton = null;
|
||||
$this->productSkeleton = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModelCategory::PIECE === $category) {
|
||||
$this->pieceSkeleton = $structure;
|
||||
$this->componentSkeleton = null;
|
||||
$this->productSkeleton = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->productSkeleton = $structure;
|
||||
$this->componentSkeleton = null;
|
||||
$this->pieceSkeleton = null;
|
||||
}
|
||||
}
|
||||
308
src/Entity/Piece.php
Normal file
308
src/Entity/Piece.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\PieceRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PieceRepository::class)]
|
||||
#[ORM\Table(name: 'pieces')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['piece:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Piece
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['piece:read', 'document:list'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Groups(['piece:read', 'document:list'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?string $reference = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?string $prix = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieces')]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true)]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?ModelType $typePiece = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'pieces')]
|
||||
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productIds')]
|
||||
#[Groups(['piece:read'])]
|
||||
private ?array $productIds = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'pieces')]
|
||||
#[ORM\JoinTable(
|
||||
name: '_PieceConstructeurs',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
)]
|
||||
#[Groups(['piece:read'])]
|
||||
private Collection $constructeurs;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Document>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: Document::class)]
|
||||
#[Groups(['piece:read'])]
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: CustomFieldValue::class)]
|
||||
#[Groups(['piece:read'])]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['piece:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['piece:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReference(): ?string
|
||||
{
|
||||
return $this->reference;
|
||||
}
|
||||
|
||||
public function setReference(?string $reference): static
|
||||
{
|
||||
$this->reference = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrix(): ?string
|
||||
{
|
||||
return $this->prix;
|
||||
}
|
||||
|
||||
public function setPrix(?string $prix): static
|
||||
{
|
||||
$this->prix = $prix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ?ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(?ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
if ($product && empty($this->productIds)) {
|
||||
$productId = $product->getId();
|
||||
$this->productIds = $productId ? [$productId] : null;
|
||||
}
|
||||
|
||||
if (!$product && empty($this->productIds)) {
|
||||
$this->productIds = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getProductIds(): array
|
||||
{
|
||||
if (!is_array($this->productIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter(
|
||||
array_map(
|
||||
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||
$this->productIds,
|
||||
),
|
||||
static fn (string $value) => '' !== $value,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function setProductIds(?array $productIds): static
|
||||
{
|
||||
if (!is_array($productIds)) {
|
||||
$this->productIds = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$normalized = array_values(
|
||||
array_unique(
|
||||
array_filter(
|
||||
array_map(
|
||||
static fn ($value) => is_string($value) ? trim($value) : '',
|
||||
$productIds,
|
||||
),
|
||||
static fn (string $value) => '' !== $value,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->productIds = [] === $normalized ? null : $normalized;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Constructeur>
|
||||
*/
|
||||
public function getConstructeurs(): Collection
|
||||
{
|
||||
return $this->constructeurs;
|
||||
}
|
||||
|
||||
public function addConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
if (!$this->constructeurs->contains($constructeur)) {
|
||||
$this->constructeurs->add($constructeur);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
$this->constructeurs->removeElement($constructeur);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Document>
|
||||
*/
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomFieldValue>
|
||||
*/
|
||||
public function getCustomFieldValues(): Collection
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
247
src/Entity/Product.php
Normal file
247
src/Entity/Product.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\ProductRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProductRepository::class)]
|
||||
#[ORM\Table(name: 'products')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['product:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Product
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['product:read', 'document:list'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Groups(['product:read', 'document:list'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['product:read'])]
|
||||
private ?string $reference = null;
|
||||
|
||||
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'supplierPrice')]
|
||||
#[Groups(['product:read'])]
|
||||
private ?string $supplierPrice = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'products')]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true)]
|
||||
#[Groups(['product:read'])]
|
||||
private ?ModelType $typeProduct = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Constructeur>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'products')]
|
||||
#[ORM\JoinTable(
|
||||
name: '_ProductConstructeurs',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
)]
|
||||
#[Groups(['product:read'])]
|
||||
private Collection $constructeurs;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Document>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Document::class)]
|
||||
#[Groups(['product:read'])]
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomFieldValue>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: CustomFieldValue::class)]
|
||||
#[Groups(['product:read'])]
|
||||
private Collection $customFieldValues;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Piece>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Piece::class)]
|
||||
private Collection $pieces;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Composant>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Composant::class)]
|
||||
private Collection $composants;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'product', targetEntity: MachineProductLink::class)]
|
||||
private Collection $machineLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['product:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['product:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->constructeurs = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->customFieldValues = new ArrayCollection();
|
||||
$this->pieces = new ArrayCollection();
|
||||
$this->composants = new ArrayCollection();
|
||||
$this->machineLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReference(): ?string
|
||||
{
|
||||
return $this->reference;
|
||||
}
|
||||
|
||||
public function setReference(?string $reference): static
|
||||
{
|
||||
$this->reference = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSupplierPrice(): ?string
|
||||
{
|
||||
return $this->supplierPrice;
|
||||
}
|
||||
|
||||
public function setSupplierPrice(?string $supplierPrice): static
|
||||
{
|
||||
$this->supplierPrice = $supplierPrice;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ?ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(?ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Constructeur>
|
||||
*/
|
||||
public function getConstructeurs(): Collection
|
||||
{
|
||||
return $this->constructeurs;
|
||||
}
|
||||
|
||||
public function addConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
if (!$this->constructeurs->contains($constructeur)) {
|
||||
$this->constructeurs->add($constructeur);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeConstructeur(Constructeur $constructeur): static
|
||||
{
|
||||
$this->constructeurs->removeElement($constructeur);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Document>
|
||||
*/
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomFieldValue>
|
||||
*/
|
||||
public function getCustomFieldValues(): Collection
|
||||
{
|
||||
return $this->customFieldValues;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
223
src/Entity/Profile.php
Normal file
223
src/Entity/Profile.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProfileRepository::class)]
|
||||
#[ORM\Table(name: 'profiles')]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_email', columns: ['email'])]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(),
|
||||
new Post(),
|
||||
new Put(),
|
||||
new Delete(),
|
||||
],
|
||||
normalizationContext: ['groups' => ['profile:read']],
|
||||
denormalizationContext: ['groups' => ['profile:write']]
|
||||
)]
|
||||
class Profile implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'string', length: 36)]
|
||||
#[Groups(['profile:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 180, unique: true, nullable: true)]
|
||||
#[Assert\Email]
|
||||
#[Groups(['profile:read', 'profile:write'])]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, name: 'firstname')]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['profile:read', 'profile:write'])]
|
||||
private string $firstName;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, name: 'lastname')]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['profile:read', 'profile:write'])]
|
||||
private string $lastName;
|
||||
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => true], name: 'isactive')]
|
||||
#[Groups(['profile:read', 'profile:write'])]
|
||||
private bool $isActive = true;
|
||||
|
||||
/**
|
||||
* @var list<string> The user roles
|
||||
*/
|
||||
#[ORM\Column(type: 'json', options: ['default' => '["ROLE_USER"]'])]
|
||||
#[Groups(['profile:read', 'profile:write'])]
|
||||
private array $roles = ['ROLE_USER'];
|
||||
|
||||
/**
|
||||
* @var string The hashed password
|
||||
*/
|
||||
#[ORM\Column(type: 'string', nullable: true)]
|
||||
#[Groups(['profile:write'])]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', name: 'createdat')]
|
||||
#[Groups(['profile:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', name: 'updatedat')]
|
||||
#[Groups(['profile:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Générer un CUID-like ID pour compatibilité avec Prisma
|
||||
$this->id = 'cl'.substr(strtolower(base_convert(random_bytes(12), 2, 36)), 0, 24);
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFirstName(): string
|
||||
{
|
||||
return $this->firstName;
|
||||
}
|
||||
|
||||
public function setFirstName(string $firstName): static
|
||||
{
|
||||
$this->firstName = $firstName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastName(): string
|
||||
{
|
||||
return $this->lastName;
|
||||
}
|
||||
|
||||
public function setLastName(string $lastName): static
|
||||
{
|
||||
$this->lastName = $lastName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function setIsActive(bool $isActive): static
|
||||
{
|
||||
$this->isActive = $isActive;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
266
src/Entity/Site.php
Normal file
266
src/Entity/Site.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\SiteRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: SiteRepository::class)]
|
||||
#[ORM\Table(name: 'sites')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(),
|
||||
new Post(),
|
||||
new Put(),
|
||||
new Delete(),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Site
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['document:list'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['document:list'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')]
|
||||
private string $contactName = '';
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 20, options: ['default' => ''], name: 'contactPhone')]
|
||||
private string $contactPhone = '';
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 500, options: ['default' => ''], name: 'contactAddress')]
|
||||
private string $contactAddress = '';
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 10, options: ['default' => ''], name: 'contactPostalCode')]
|
||||
private string $contactPostalCode = '';
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 100, options: ['default' => ''], name: 'contactCity')]
|
||||
private string $contactCity = '';
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Machine>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Machine::class, mappedBy: 'site', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $machines;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Document>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Document::class, mappedBy: 'site', cascade: ['remove'], orphanRemoval: true)]
|
||||
private Collection $documents;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machines = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
|
||||
// Générer un ID CUID-compatible si nécessaire
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContactName(): string
|
||||
{
|
||||
return $this->contactName;
|
||||
}
|
||||
|
||||
public function setContactName(string $contactName): static
|
||||
{
|
||||
$this->contactName = $contactName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContactPhone(): string
|
||||
{
|
||||
return $this->contactPhone;
|
||||
}
|
||||
|
||||
public function setContactPhone(string $contactPhone): static
|
||||
{
|
||||
$this->contactPhone = $contactPhone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContactAddress(): string
|
||||
{
|
||||
return $this->contactAddress;
|
||||
}
|
||||
|
||||
public function setContactAddress(string $contactAddress): static
|
||||
{
|
||||
$this->contactAddress = $contactAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContactPostalCode(): string
|
||||
{
|
||||
return $this->contactPostalCode;
|
||||
}
|
||||
|
||||
public function setContactPostalCode(string $contactPostalCode): static
|
||||
{
|
||||
$this->contactPostalCode = $contactPostalCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContactCity(): string
|
||||
{
|
||||
return $this->contactCity;
|
||||
}
|
||||
|
||||
public function setContactCity(string $contactCity): static
|
||||
{
|
||||
$this->contactCity = $contactCity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Machine>
|
||||
*/
|
||||
public function getMachines(): Collection
|
||||
{
|
||||
return $this->machines;
|
||||
}
|
||||
|
||||
public function addMachine(Machine $machine): static
|
||||
{
|
||||
if (!$this->machines->contains($machine)) {
|
||||
$this->machines->add($machine);
|
||||
$machine->setSite($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMachine(Machine $machine): static
|
||||
{
|
||||
if ($this->machines->removeElement($machine)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($machine->getSite() === $this) {
|
||||
$machine->setSite(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Document>
|
||||
*/
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
public function addDocument(Document $document): static
|
||||
{
|
||||
if (!$this->documents->contains($document)) {
|
||||
$this->documents->add($document);
|
||||
$document->setSite($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDocument(Document $document): static
|
||||
{
|
||||
if ($this->documents->removeElement($document)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($document->getSite() === $this) {
|
||||
$document->setSite(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
// Génération d'un ID compatible CUID (format: cl + 24 caractères)
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
401
src/Entity/TypeMachine.php
Normal file
401
src/Entity/TypeMachine.php
Normal file
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\TypeMachineRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineRepository::class)]
|
||||
#[ORM\Table(name: 'type_machines')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[UniqueEntity(fields: ['name'], message: 'Ce nom de type de machine existe déjà.')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(),
|
||||
new Post(),
|
||||
new Put(),
|
||||
new Delete(),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class TypeMachine
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['type_machine:read', 'type_machine:write', 'machine:read'])]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $category = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?string $maintenanceFrequency = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $components = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $criticalParts = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $machinePieces = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['type_machine:read', 'type_machine:write'])]
|
||||
private ?array $specifications = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Machine>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Machine::class, mappedBy: 'typeMachine')]
|
||||
private Collection $machines;
|
||||
|
||||
/**
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $customFields;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachineComponentRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $componentRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachinePieceRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $pieceRequirements;
|
||||
|
||||
/**
|
||||
* @var Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TypeMachineProductRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
|
||||
#[ApiProperty(readableLink: true, writableLink: true)]
|
||||
private Collection $productRequirements;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = 'cl'.bin2hex(random_bytes(12));
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
$this->machines = new ArrayCollection();
|
||||
$this->customFields = new ArrayCollection();
|
||||
$this->componentRequirements = new ArrayCollection();
|
||||
$this->pieceRequirements = new ArrayCollection();
|
||||
$this->productRequirements = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategory(): ?string
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
public function setCategory(?string $category): static
|
||||
{
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaintenanceFrequency(): ?string
|
||||
{
|
||||
return $this->maintenanceFrequency;
|
||||
}
|
||||
|
||||
public function setMaintenanceFrequency(?string $maintenanceFrequency): static
|
||||
{
|
||||
$this->maintenanceFrequency = $maintenanceFrequency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComponents(): ?array
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
public function setComponents(?array $components): static
|
||||
{
|
||||
$this->components = $components;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCriticalParts(): ?array
|
||||
{
|
||||
return $this->criticalParts;
|
||||
}
|
||||
|
||||
public function setCriticalParts(?array $criticalParts): static
|
||||
{
|
||||
$this->criticalParts = $criticalParts;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMachinePieces(): ?array
|
||||
{
|
||||
return $this->machinePieces;
|
||||
}
|
||||
|
||||
public function setMachinePieces(?array $machinePieces): static
|
||||
{
|
||||
$this->machinePieces = $machinePieces;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSpecifications(): ?array
|
||||
{
|
||||
return $this->specifications;
|
||||
}
|
||||
|
||||
public function setSpecifications(?array $specifications): static
|
||||
{
|
||||
$this->specifications = $specifications;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Machine>
|
||||
*/
|
||||
public function getMachines(): Collection
|
||||
{
|
||||
return $this->machines;
|
||||
}
|
||||
|
||||
public function addMachine(Machine $machine): static
|
||||
{
|
||||
if (!$this->machines->contains($machine)) {
|
||||
$this->machines->add($machine);
|
||||
$machine->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMachine(Machine $machine): static
|
||||
{
|
||||
if ($this->machines->removeElement($machine)) {
|
||||
if ($machine->getTypeMachine() === $this) {
|
||||
$machine->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, CustomField>
|
||||
*/
|
||||
public function getCustomFields(): Collection
|
||||
{
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
public function addCustomField(CustomField $customField): static
|
||||
{
|
||||
if (!$this->customFields->contains($customField)) {
|
||||
$this->customFields->add($customField);
|
||||
$customField->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCustomField(CustomField $customField): static
|
||||
{
|
||||
if ($this->customFields->removeElement($customField)) {
|
||||
if ($customField->getTypeMachine() === $this) {
|
||||
$customField->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachineComponentRequirement>
|
||||
*/
|
||||
public function getComponentRequirements(): Collection
|
||||
{
|
||||
return $this->componentRequirements;
|
||||
}
|
||||
|
||||
public function addComponentRequirement(TypeMachineComponentRequirement $componentRequirement): static
|
||||
{
|
||||
if (!$this->componentRequirements->contains($componentRequirement)) {
|
||||
$this->componentRequirements->add($componentRequirement);
|
||||
$componentRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeComponentRequirement(TypeMachineComponentRequirement $componentRequirement): static
|
||||
{
|
||||
if ($this->componentRequirements->removeElement($componentRequirement)) {
|
||||
if ($componentRequirement->getTypeMachine() === $this) {
|
||||
$componentRequirement->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachinePieceRequirement>
|
||||
*/
|
||||
public function getPieceRequirements(): Collection
|
||||
{
|
||||
return $this->pieceRequirements;
|
||||
}
|
||||
|
||||
public function addPieceRequirement(TypeMachinePieceRequirement $pieceRequirement): static
|
||||
{
|
||||
if (!$this->pieceRequirements->contains($pieceRequirement)) {
|
||||
$this->pieceRequirements->add($pieceRequirement);
|
||||
$pieceRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePieceRequirement(TypeMachinePieceRequirement $pieceRequirement): static
|
||||
{
|
||||
if ($this->pieceRequirements->removeElement($pieceRequirement)) {
|
||||
if ($pieceRequirement->getTypeMachine() === $this) {
|
||||
$pieceRequirement->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, TypeMachineProductRequirement>
|
||||
*/
|
||||
public function getProductRequirements(): Collection
|
||||
{
|
||||
return $this->productRequirements;
|
||||
}
|
||||
|
||||
public function addProductRequirement(TypeMachineProductRequirement $productRequirement): static
|
||||
{
|
||||
if (!$this->productRequirements->contains($productRequirement)) {
|
||||
$this->productRequirements->add($productRequirement);
|
||||
$productRequirement->setTypeMachine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductRequirement(TypeMachineProductRequirement $productRequirement): static
|
||||
{
|
||||
if ($this->productRequirements->removeElement($productRequirement)) {
|
||||
if ($productRequirement->getTypeMachine() === $this) {
|
||||
$productRequirement->setTypeMachine(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
209
src/Entity/TypeMachineComponentRequirement.php
Normal file
209
src/Entity/TypeMachineComponentRequirement.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\TypeMachineComponentRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineComponentRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_component_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class TypeMachineComponentRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 1;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'required')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = true;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'componentRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'componentRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typeComposant;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineComponentLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachineComponentRequirement', targetEntity: MachineComponentLink::class)]
|
||||
private Collection $machineComponentLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machineComponentLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeComposant(): ModelType
|
||||
{
|
||||
return $this->typeComposant;
|
||||
}
|
||||
|
||||
public function setTypeComposant(ModelType $typeComposant): static
|
||||
{
|
||||
$this->typeComposant = $typeComposant;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
209
src/Entity/TypeMachinePieceRequirement.php
Normal file
209
src/Entity/TypeMachinePieceRequirement.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\TypeMachinePieceRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachinePieceRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_piece_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class TypeMachinePieceRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 0;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = false;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'pieceRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typePiece;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachinePieceLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachinePieceRequirement', targetEntity: MachinePieceLink::class)]
|
||||
private Collection $machinePieceLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machinePieceLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypePiece(): ModelType
|
||||
{
|
||||
return $this->typePiece;
|
||||
}
|
||||
|
||||
public function setTypePiece(ModelType $typePiece): static
|
||||
{
|
||||
$this->typePiece = $typePiece;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
209
src/Entity/TypeMachineProductRequirement.php
Normal file
209
src/Entity/TypeMachineProductRequirement.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\TypeMachineProductRequirementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TypeMachineProductRequirementRepository::class)]
|
||||
#[ORM\Table(name: 'type_machine_product_requirements')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
class TypeMachineProductRequirement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: Types::STRING, length: 36)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'minCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $minCount = 0;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true, name: 'maxCount')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ?int $maxCount = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $required = false;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true], name: 'allowNewModels')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private bool $allowNewModels = true;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private int $orderIndex = 0;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'productRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeMachineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private TypeMachine $typeMachine;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productRequirements')]
|
||||
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: false)]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
#[Groups(['type_machine:read'])]
|
||||
private ModelType $typeProduct;
|
||||
|
||||
/**
|
||||
* @var Collection<int, MachineProductLink>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'typeMachineProductRequirement', targetEntity: MachineProductLink::class)]
|
||||
private Collection $machineProductLinks;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->machineProductLinks = new ArrayCollection();
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
if (null === $this->id) {
|
||||
$this->id = $this->generateCuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(string $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinCount(): int
|
||||
{
|
||||
return $this->minCount;
|
||||
}
|
||||
|
||||
public function setMinCount(int $minCount): static
|
||||
{
|
||||
$this->minCount = $minCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxCount(): ?int
|
||||
{
|
||||
return $this->maxCount;
|
||||
}
|
||||
|
||||
public function setMaxCount(?int $maxCount): static
|
||||
{
|
||||
$this->maxCount = $maxCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): static
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAllowNewModels(): bool
|
||||
{
|
||||
return $this->allowNewModels;
|
||||
}
|
||||
|
||||
public function setAllowNewModels(bool $allowNewModels): static
|
||||
{
|
||||
$this->allowNewModels = $allowNewModels;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrderIndex(): int
|
||||
{
|
||||
return $this->orderIndex;
|
||||
}
|
||||
|
||||
public function setOrderIndex(int $orderIndex): static
|
||||
{
|
||||
$this->orderIndex = $orderIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeMachine(): TypeMachine
|
||||
{
|
||||
return $this->typeMachine;
|
||||
}
|
||||
|
||||
public function setTypeMachine(TypeMachine $typeMachine): static
|
||||
{
|
||||
$this->typeMachine = $typeMachine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeProduct(): ModelType
|
||||
{
|
||||
return $this->typeProduct;
|
||||
}
|
||||
|
||||
public function setTypeProduct(ModelType $typeProduct): static
|
||||
{
|
||||
$this->typeProduct = $typeProduct;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
12
src/Enum/ModelCategory.php
Normal file
12
src/Enum/ModelCategory.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum ModelCategory: string
|
||||
{
|
||||
case COMPONENT = 'COMPONENT';
|
||||
case PIECE = 'PIECE';
|
||||
case PRODUCT = 'PRODUCT';
|
||||
}
|
||||
54
src/EventListener/DocumentPdfCompressorListener.php
Normal file
54
src/EventListener/DocumentPdfCompressorListener.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Document;
|
||||
use App\Service\PdfCompressorService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Events;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: Document::class)]
|
||||
#[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: Document::class)]
|
||||
class DocumentPdfCompressorListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PdfCompressorService $pdfCompressor,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function prePersist(Document $document): void
|
||||
{
|
||||
$this->compressIfPdf($document);
|
||||
}
|
||||
|
||||
public function preUpdate(Document $document): void
|
||||
{
|
||||
$this->compressIfPdf($document);
|
||||
}
|
||||
|
||||
private function compressIfPdf(Document $document): void
|
||||
{
|
||||
if ('application/pdf' !== $document->getMimeType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->pdfCompressor->compressBase64Pdf($document->getPath());
|
||||
|
||||
if (null === $result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$document->setPath($result['path']);
|
||||
$document->setSize($result['size']);
|
||||
|
||||
$this->logger?->info('PDF compressed', [
|
||||
'document' => $document->getName(),
|
||||
'originalSize' => $result['originalSize'],
|
||||
'compressedSize' => $result['size'],
|
||||
'saved' => $result['saved'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
397
src/EventSubscriber/ComposantAuditSubscriber.php
Normal file
397
src/EventSubscriber/ComposantAuditSubscriber.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\AuditLog;
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\Profile;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Throwable;
|
||||
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function method_exists;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
final class ComposantAuditSubscriber implements EventSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
$pendingUpdates = [];
|
||||
$pendingSnapshots = [];
|
||||
$pendingComponents = [];
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Composant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
$snapshot = $this->snapshotComposant($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Composant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$componentId = (string) $entity->getId();
|
||||
if ('' === $componentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
if ([] !== $diff) {
|
||||
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
||||
$pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
|
||||
$pendingComponents[$componentId] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if (!$entity instanceof Composant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->snapshotComposant($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
|
||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
}
|
||||
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
|
||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
}
|
||||
|
||||
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
|
||||
foreach ($pendingUpdates as $componentId => $diff) {
|
||||
if ([] === $diff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$component = $pendingComponents[$componentId] ?? null;
|
||||
if (!$component instanceof Composant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $pendingSnapshots[$componentId] ?? $this->snapshotComposant($component);
|
||||
$this->persistAuditLog($em, new AuditLog('composant', $componentId, 'update', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $pendingComponents
|
||||
*/
|
||||
private function collectCollectionUpdate(
|
||||
object $collection,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingComponents,
|
||||
): void {
|
||||
if (!$collection instanceof PersistentCollection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$owner = $collection->getOwner();
|
||||
if (!$owner instanceof Composant) {
|
||||
return;
|
||||
}
|
||||
|
||||
$componentId = (string) $owner->getId();
|
||||
if ('' === $componentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mapping = $collection->getMapping();
|
||||
$fieldName = $mapping['fieldName'] ?? null;
|
||||
if ('constructeurs' !== $fieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||
$after = $this->normalizeCollection($collection->toArray());
|
||||
|
||||
if ($before === $after) {
|
||||
return;
|
||||
}
|
||||
|
||||
$diff = [
|
||||
'constructeurIds' => [
|
||||
'from' => $before,
|
||||
'to' => $after,
|
||||
],
|
||||
];
|
||||
|
||||
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
|
||||
$pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
|
||||
$pendingComponents[$componentId] = $owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $pendingComponents
|
||||
*/
|
||||
private function collectCustomFieldValueChanges(
|
||||
UnitOfWork $uow,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingComponents,
|
||||
): void {
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if ($entity instanceof CustomFieldValue) {
|
||||
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof CustomFieldValue) {
|
||||
continue;
|
||||
}
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
if (!isset($changeSet['value'])) {
|
||||
continue;
|
||||
}
|
||||
[$oldVal, $newVal] = $changeSet['value'];
|
||||
if ($oldVal !== $newVal) {
|
||||
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if ($entity instanceof CustomFieldValue) {
|
||||
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingComponents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $pendingComponents
|
||||
*/
|
||||
private function trackCustomFieldValueChange(
|
||||
CustomFieldValue $cfv,
|
||||
mixed $from,
|
||||
mixed $to,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingComponents,
|
||||
): void {
|
||||
$owner = $cfv->getComposant();
|
||||
if (!$owner instanceof Composant) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ownerId = (string) $owner->getId();
|
||||
if ('' === $ownerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||
|
||||
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||
$pendingSnapshots[$ownerId] = $this->snapshotComposant($owner);
|
||||
$pendingComponents[$ownerId] = $owner;
|
||||
}
|
||||
|
||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||
{
|
||||
$uow = $em->getUnitOfWork();
|
||||
$log->initializeAuditLog();
|
||||
$em->persist($log);
|
||||
|
||||
$meta = $em->getClassMetadata(AuditLog::class);
|
||||
$uow->computeChangeSet($meta, $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function buildDiffFromChangeSet(array $changeSet): array
|
||||
{
|
||||
$diff = [];
|
||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedOld = $this->normalizeValue($oldValue);
|
||||
$normalizedNew = $this->normalizeValue($newValue);
|
||||
|
||||
if ($normalizedOld === $normalizedNew) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff[$field] = [
|
||||
'from' => $normalizedOld,
|
||||
'to' => $normalizedNew,
|
||||
];
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
private function snapshotComposant(Composant $component): array
|
||||
{
|
||||
return [
|
||||
'id' => $component->getId(),
|
||||
'name' => $component->getName(),
|
||||
'reference' => $component->getReference(),
|
||||
'prix' => $component->getPrix(),
|
||||
'structure' => $component->getStructure(),
|
||||
'typeComposant' => $this->normalizeValue($component->getTypeComposant()),
|
||||
'product' => $this->normalizeValue($component->getProduct()),
|
||||
'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<mixed> $items
|
||||
*
|
||||
* @return list<array{id: string, name: string}|string>
|
||||
*/
|
||||
private function normalizeCollection(iterable $items): array
|
||||
{
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
foreach ($items as $item) {
|
||||
if (is_object($item) && method_exists($item, 'getId')) {
|
||||
$id = $item->getId();
|
||||
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||
continue;
|
||||
}
|
||||
$seen[(string) $id] = true;
|
||||
if (method_exists($item, 'getName')) {
|
||||
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||
} else {
|
||||
$entries[] = (string) $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function normalizeValue(mixed $value): mixed
|
||||
{
|
||||
if (null === $value || is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
if ($value instanceof ModelType) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
'code' => $value->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof Product) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
'reference' => $value->getReference(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof Collection) {
|
||||
return $this->normalizeCollection($value);
|
||||
}
|
||||
|
||||
if (is_object($value) && method_exists($value, 'getId')) {
|
||||
return (string) $value->getId();
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function mergeDiffs(array $base, array $extra): array
|
||||
{
|
||||
foreach ($extra as $field => $change) {
|
||||
$base[$field] = $change;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function resolveActorProfileId(): ?string
|
||||
{
|
||||
try {
|
||||
$session = $this->requestStack->getSession();
|
||||
if ($session instanceof SessionInterface) {
|
||||
$profileId = $session->get('profileId');
|
||||
if ($profileId) {
|
||||
return (string) $profileId;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// No session available (CLI context, etc.)
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof Profile) {
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\AuditLog;
|
||||
use App\Entity\Constructeur;
|
||||
use App\Entity\Profile;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Throwable;
|
||||
|
||||
use function is_scalar;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
final class ConstructeurAuditSubscriber implements EventSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Constructeur) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
$snapshot = $this->snapshotConstructeur($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Constructeur) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = (string) $entity->getId();
|
||||
if ('' === $id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
if ([] !== $diff) {
|
||||
$snapshot = $this->snapshotConstructeur($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('constructeur', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if (!$entity instanceof Constructeur) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->snapshotConstructeur($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||
{
|
||||
$uow = $em->getUnitOfWork();
|
||||
$log->initializeAuditLog();
|
||||
$em->persist($log);
|
||||
|
||||
$meta = $em->getClassMetadata(AuditLog::class);
|
||||
$uow->computeChangeSet($meta, $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function buildDiffFromChangeSet(array $changeSet): array
|
||||
{
|
||||
$diff = [];
|
||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedOld = $this->normalizeValue($oldValue);
|
||||
$normalizedNew = $this->normalizeValue($newValue);
|
||||
|
||||
if ($normalizedOld === $normalizedNew) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff[$field] = [
|
||||
'from' => $normalizedOld,
|
||||
'to' => $normalizedNew,
|
||||
];
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
private function snapshotConstructeur(Constructeur $constructeur): array
|
||||
{
|
||||
return [
|
||||
'id' => $constructeur->getId(),
|
||||
'name' => $constructeur->getName(),
|
||||
'email' => $constructeur->getEmail(),
|
||||
'phone' => $constructeur->getPhone(),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeValue(mixed $value): mixed
|
||||
{
|
||||
if (null === $value || is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function resolveActorProfileId(): ?string
|
||||
{
|
||||
try {
|
||||
$session = $this->requestStack->getSession();
|
||||
if ($session instanceof SessionInterface) {
|
||||
$profileId = $session->get('profileId');
|
||||
if ($profileId) {
|
||||
return (string) $profileId;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// No session available (CLI context, etc.)
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof Profile) {
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\AuditLog;
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\Document;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\Profile;
|
||||
use App\Entity\Site;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Throwable;
|
||||
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function method_exists;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
final class DocumentAuditSubscriber implements EventSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
$snapshot = $this->snapshotDocument($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = (string) $entity->getId();
|
||||
if ('' === $id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
if ([] !== $diff) {
|
||||
$snapshot = $this->snapshotDocument($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('document', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if (!$entity instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->snapshotDocument($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||
{
|
||||
$uow = $em->getUnitOfWork();
|
||||
$log->initializeAuditLog();
|
||||
$em->persist($log);
|
||||
|
||||
$meta = $em->getClassMetadata(AuditLog::class);
|
||||
$uow->computeChangeSet($meta, $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function buildDiffFromChangeSet(array $changeSet): array
|
||||
{
|
||||
$diff = [];
|
||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedOld = $this->normalizeValue($oldValue);
|
||||
$normalizedNew = $this->normalizeValue($newValue);
|
||||
|
||||
if ($normalizedOld === $normalizedNew) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff[$field] = [
|
||||
'from' => $normalizedOld,
|
||||
'to' => $normalizedNew,
|
||||
];
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
private function snapshotDocument(Document $document): array
|
||||
{
|
||||
return [
|
||||
'id' => $document->getId(),
|
||||
'name' => $document->getName(),
|
||||
'filename' => $document->getFilename(),
|
||||
'mimeType' => $document->getMimeType(),
|
||||
'size' => $document->getSize(),
|
||||
'machine' => $this->normalizeValue($document->getMachine()),
|
||||
'composant' => $this->normalizeValue($document->getComposant()),
|
||||
'piece' => $this->normalizeValue($document->getPiece()),
|
||||
'product' => $this->normalizeValue($document->getProduct()),
|
||||
'site' => $this->normalizeValue($document->getSite()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeValue(mixed $value): mixed
|
||||
{
|
||||
if (null === $value || is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
if ($value instanceof Machine || $value instanceof Composant || $value instanceof Piece || $value instanceof Product || $value instanceof Site) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if (is_object($value) && method_exists($value, 'getId')) {
|
||||
return (string) $value->getId();
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function resolveActorProfileId(): ?string
|
||||
{
|
||||
try {
|
||||
$session = $this->requestStack->getSession();
|
||||
if ($session instanceof SessionInterface) {
|
||||
$profileId = $session->get('profileId');
|
||||
if ($profileId) {
|
||||
return (string) $profileId;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// No session available (CLI context, etc.)
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof Profile) {
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\AuditLog;
|
||||
use App\Entity\CustomFieldValue;
|
||||
use App\Entity\Machine;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\Profile;
|
||||
use App\Entity\Site;
|
||||
use App\Entity\TypeMachine;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Throwable;
|
||||
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function method_exists;
|
||||
|
||||
#[AsDoctrineListener(event: Events::onFlush)]
|
||||
final class MachineAuditSubscriber implements EventSubscriber
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$em = $args->getObjectManager();
|
||||
if (!$em instanceof EntityManagerInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uow = $em->getUnitOfWork();
|
||||
$actorProfileId = $this->resolveActorProfileId();
|
||||
$pendingUpdates = [];
|
||||
$pendingSnapshots = [];
|
||||
$pendingMachines = [];
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!$entity instanceof Machine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
$snapshot = $this->snapshotMachine($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Machine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$machineId = (string) $entity->getId();
|
||||
if ('' === $machineId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||
if ([] !== $diff) {
|
||||
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||
$pendingSnapshots[$machineId] = $this->snapshotMachine($entity);
|
||||
$pendingMachines[$machineId] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if (!$entity instanceof Machine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $this->snapshotMachine($entity);
|
||||
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
|
||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
}
|
||||
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
|
||||
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
}
|
||||
|
||||
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
|
||||
foreach ($pendingUpdates as $machineId => $diff) {
|
||||
if ([] === $diff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$machine = $pendingMachines[$machineId] ?? null;
|
||||
if (!$machine instanceof Machine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = $pendingSnapshots[$machineId] ?? $this->snapshotMachine($machine);
|
||||
$this->persistAuditLog($em, new AuditLog('machine', $machineId, 'update', $diff, $snapshot, $actorProfileId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Machine> $pendingMachines
|
||||
*/
|
||||
private function collectCollectionUpdate(
|
||||
object $collection,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingMachines,
|
||||
): void {
|
||||
if (!$collection instanceof PersistentCollection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$owner = $collection->getOwner();
|
||||
if (!$owner instanceof Machine) {
|
||||
return;
|
||||
}
|
||||
|
||||
$machineId = (string) $owner->getId();
|
||||
if ('' === $machineId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mapping = $collection->getMapping();
|
||||
$fieldName = $mapping['fieldName'] ?? null;
|
||||
if ('constructeurs' !== $fieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||
$after = $this->normalizeCollection($collection->toArray());
|
||||
|
||||
if ($before === $after) {
|
||||
return;
|
||||
}
|
||||
|
||||
$diff = [
|
||||
'constructeurIds' => [
|
||||
'from' => $before,
|
||||
'to' => $after,
|
||||
],
|
||||
];
|
||||
|
||||
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||
$pendingSnapshots[$machineId] = $this->snapshotMachine($owner);
|
||||
$pendingMachines[$machineId] = $owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Machine> $pendingMachines
|
||||
*/
|
||||
private function collectCustomFieldValueChanges(
|
||||
UnitOfWork $uow,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingMachines,
|
||||
): void {
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if ($entity instanceof CustomFieldValue) {
|
||||
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof CustomFieldValue) {
|
||||
continue;
|
||||
}
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
if (!isset($changeSet['value'])) {
|
||||
continue;
|
||||
}
|
||||
[$oldVal, $newVal] = $changeSet['value'];
|
||||
if ($oldVal !== $newVal) {
|
||||
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if ($entity instanceof CustomFieldValue) {
|
||||
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Machine> $pendingMachines
|
||||
*/
|
||||
private function trackCustomFieldValueChange(
|
||||
CustomFieldValue $cfv,
|
||||
mixed $from,
|
||||
mixed $to,
|
||||
array &$pendingUpdates,
|
||||
array &$pendingSnapshots,
|
||||
array &$pendingMachines,
|
||||
): void {
|
||||
$owner = $cfv->getMachine();
|
||||
if (!$owner instanceof Machine) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ownerId = (string) $owner->getId();
|
||||
if ('' === $ownerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||
|
||||
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||
$pendingSnapshots[$ownerId] = $this->snapshotMachine($owner);
|
||||
$pendingMachines[$ownerId] = $owner;
|
||||
}
|
||||
|
||||
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||
{
|
||||
$uow = $em->getUnitOfWork();
|
||||
$log->initializeAuditLog();
|
||||
$em->persist($log);
|
||||
|
||||
$meta = $em->getClassMetadata(AuditLog::class);
|
||||
$uow->computeChangeSet($meta, $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function buildDiffFromChangeSet(array $changeSet): array
|
||||
{
|
||||
$diff = [];
|
||||
foreach ($changeSet as $field => [$oldValue, $newValue]) {
|
||||
if ('updatedAt' === $field || 'createdAt' === $field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalizedOld = $this->normalizeValue($oldValue);
|
||||
$normalizedNew = $this->normalizeValue($newValue);
|
||||
|
||||
if ($normalizedOld === $normalizedNew) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$diff[$field] = [
|
||||
'from' => $normalizedOld,
|
||||
'to' => $normalizedNew,
|
||||
];
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
private function snapshotMachine(Machine $machine): array
|
||||
{
|
||||
return [
|
||||
'id' => $machine->getId(),
|
||||
'name' => $machine->getName(),
|
||||
'reference' => $machine->getReference(),
|
||||
'prix' => $machine->getPrix(),
|
||||
'site' => $this->normalizeValue($machine->getSite()),
|
||||
'typeMachine' => $this->normalizeValue($machine->getTypeMachine()),
|
||||
'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<mixed> $items
|
||||
*
|
||||
* @return list<array{id: string, name: string}|string>
|
||||
*/
|
||||
private function normalizeCollection(iterable $items): array
|
||||
{
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
foreach ($items as $item) {
|
||||
if (is_object($item) && method_exists($item, 'getId')) {
|
||||
$id = $item->getId();
|
||||
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||
continue;
|
||||
}
|
||||
$seen[(string) $id] = true;
|
||||
if (method_exists($item, 'getName')) {
|
||||
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||
} else {
|
||||
$entries[] = (string) $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
private function normalizeValue(mixed $value): mixed
|
||||
{
|
||||
if (null === $value || is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
if ($value instanceof Site) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof TypeMachine) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof ModelType) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
'code' => $value->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof Product) {
|
||||
return [
|
||||
'id' => $value->getId(),
|
||||
'name' => $value->getName(),
|
||||
'reference' => $value->getReference(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($value instanceof Collection) {
|
||||
return $this->normalizeCollection($value);
|
||||
}
|
||||
|
||||
if (is_object($value) && method_exists($value, 'getId')) {
|
||||
return (string) $value->getId();
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function mergeDiffs(array $base, array $extra): array
|
||||
{
|
||||
foreach ($extra as $field => $change) {
|
||||
$base[$field] = $change;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function resolveActorProfileId(): ?string
|
||||
{
|
||||
try {
|
||||
$session = $this->requestStack->getSession();
|
||||
if ($session instanceof SessionInterface) {
|
||||
$profileId = $session->get('profileId');
|
||||
if ($profileId) {
|
||||
return (string) $profileId;
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// No session available (CLI context, etc.)
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof Profile) {
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user