Compare commits

...

38 Commits

Author SHA1 Message Date
Matthieu
b51671b1d4 chore(release) : v1.1.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 15:58:09 +01:00
Matthieu
1643dcf8c2 fix : case-insensitive search filters for all entities 2026-01-25 15:54:07 +01:00
Matthieu
17ab4cdd16 chore : update fixtures with current database data 2026-01-25 15:44:22 +01:00
Matthieu
d9182131d9 chore : reset migrations to single initial schema + add deployment guide 2026-01-25 15:41:06 +01:00
Matthieu
26a7fe64be chore : update frontend submodule to master v1.0.0 2026-01-25 12:07:15 +01:00
Matthieu
4669dec359 chore : add versioning system and release script 2026-01-25 12:02:20 +01:00
Matthieu
3f05fe753e Update frontend submodule for piece requirements and component selections 2026-01-25 11:40:48 +01:00
Matthieu
a502acd234 Support multiple product requirements on pieces 2026-01-25 11:40:41 +01:00
Matthieu
69b199b6dc Update frontend submodule: overview machines + lighter PDF previews 2026-01-25 09:46:23 +01:00
Matthieu
d5f6749f9e wip: api filters and migration helpers 2026-01-23 23:30:05 +01:00
ad7918c993 chore(frontend) : met a jour le sous-module 2026-01-23 19:35:36 +01:00
86447000b1 feat(api) : ajoute pagination et filtres pour les catalogues 2026-01-23 19:35:26 +01:00
7da5eb917a feat(fixtures): ajouter système de fixtures pour les données
- fixtures/data.sql: dump des données actuelles de la base
- fixtures/load.sh: script shell de chargement
- makefile: targets fixtures-dump, fixtures-load, fixtures-reset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 13:34:07 +01:00
d65eb9ff0f WIP: corrections sérialisation API et script normalisation SQL
Backend:
- Fix Groups sur TypeMachine*Requirement: exposer typePiece/typeComposant/typeProduct
- Fix Groups sur Document, Piece, Product, Composant pour sérialisation
- Add addConstructeur/removeConstructeur sur Piece et Product

Scripts:
- Fix normalize-dump.py: gérer les schémas quotés ("public"."table")

Frontend (sous-module):
- Corrections formulaires et sérialisation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:34:00 +01:00
895df7415b chore(frontend): maj sous-module 2026-01-15 13:43:33 +01:00
9abe9fea7f chore(migrations): normaliser le schema 2026-01-15 13:42:36 +01:00
ea45ce9d0a fix(schema): aligner mappings et sortie machine 2026-01-15 13:42:29 +01:00
2c3fbb093a feat(modeles): exposer la structure selon la categorie 2026-01-15 13:42:18 +01:00
3c5fb83673 docs: maj migration et carnet de bord 2026-01-15 13:39:06 +01:00
e1dc8850c0 feat(make): nettoyer le cache Symfony 2026-01-15 13:38:55 +01:00
59622580a9 chore(frontend): maj sous-module 2026-01-15 12:51:38 +01:00
bdd1837247 chore(frontend): mettre a jour le sous-module 2026-01-14 23:11:05 +01:00
40b4b90ed8 wip(api) : machine skeleton + type links 2026-01-12 13:14:19 +01:00
d4bdb76fda docs: ajouter la liaison INV-20260111-02 [INV-20260111-02] 2026-01-11 17:14:53 +01:00
f7fc1bdee2 chore(repo): maj du submodule frontend [INV-20260111-02] 2026-01-11 17:14:38 +01:00
1a751927fa docs: tracer la liaison INV-20260111-01 [INV-20260111-01] 2026-01-11 17:12:46 +01:00
987aa5c15f chore(repo): lier Inventory_frontend en submodule [INV-20260111-01] 2026-01-11 17:12:34 +01:00
d2a1cd0cc4 docs: mettre a jour le plan et le carnet de bord 2026-01-11 17:06:36 +01:00
5222a6bbf9 chore(config): ajuster docker, cors et securite 2026-01-11 17:06:25 +01:00
15e0b23f15 chore(migration): ajouter scripts et migrations de donnees 2026-01-11 17:05:50 +01:00
fab1d25871 feat(api): ajouter les endpoints session, documents, champs et squelette 2026-01-11 17:05:41 +01:00
037ed782a7 feat(api): ajouter les entites et repositories inventory 2026-01-11 17:05:36 +01:00
de8b05a553 docs : mise a jour carnet de bord Session 2
- Resolution probleme Apache
- JWT 100% operationnel
- Reorganisation projet terminee
- Etat des lieux complet dans MIGRATION_PLAN
- 673 lignes de donnees a migrer identifiees

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:42:30 +01:00
6f9e1ec626 chore(config) : mise a jour configuration Symfony et Docker
- Installation bundles: lexik/jwt, vich/uploader, symfony/uid
- Configuration Docker avec pgAdmin
- Variables environnement pour JWT et PostgreSQL
- VirtualHost Apache pour Symfony public/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:42:01 +01:00
8430e9baef docs : ajout documentation migration et reorganisation projet
- Plan de migration detaille (Prisma vers Doctrine)
- Carnet de bord avec etat des lieux Phase 1-2
- Mise a jour CHANGELOG et README
- Reorganisation structure projet (frontend/)
- Gitignore pour archives et frontend

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:41:55 +01:00
fca3104a39 feat(api) : configuration API Platform avec JWT
- Configuration security.yaml avec firewalls JWT
- Routes API Platform avec prefixe /api
- Controller de test pour validation setup
- htaccess pour mod_rewrite Apache
- Access control pour routes publiques/protegees

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:41:48 +01:00
50336694f6 feat(auth) : creation entite Profile avec JWT
- Entite Profile implementant UserInterface
- Support authentification JWT via email/password
- Repository avec PasswordUpgraderInterface
- Migration Doctrine pour table profiles
- Script utilitaire creation utilisateur test

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:41:38 +01:00
c99f76d755 feat(infra) : ajout de pgAdmin avec configuration auto
- Ajout du service pgAdmin dans docker-compose.yml
- Configuration serveur PostgreSQL pre-enregistre (servers.json)
- Fichier pgpass pour authentification automatique
- Port 5050 expose pour acces web
- Configuration lexik/jwt-authentication-bundle

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 19:41:32 +01:00
93 changed files with 11658 additions and 57 deletions

6
.env
View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1,3 @@
[submodule "Inventory_frontend"]
path = Inventory_frontend
url = gitea@gitea.malio.fr:MALIO-DEV/Inventory_frontend.git

425
CARNET_DE_BORD.md Normal file
View 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

View File

@@ -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
View 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

Submodule Inventory_frontend added at adccfa9b46

1419
MIGRATION_PLAN.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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,6 +37,7 @@ 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

138
RELEASE.md Normal file
View 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
View 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.

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.1.0

View File

@@ -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."

View File

@@ -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
View File

@@ -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"
}

View File

@@ -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],
];

View File

@@ -1,7 +1,7 @@
api_platform:
title: Hello API Platform
version: 1.0.0
version: 1.1.0
defaults:
stateless: true
stateless: false
cache_headers:
vary: ['Content-Type', 'Authorization', 'Origin']

View File

@@ -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

View 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)%'

View File

@@ -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/': ~

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -1,4 +1,4 @@
api_platform:
resource: .
type: api_platform
prefix: /
prefix: /api

View File

@@ -0,0 +1,5 @@
controllers:
resource:
path: ../../src/Controller/
namespace: App\Controller
type: attribute

61
create_test_user.php Normal file
View 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";

View File

@@ -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,37 @@ services:
volumes:
- pg_data:/var/lib/postgresql/data
ports:
- "${POSTGRES_PORT:-5432}:5432"
- "${POSTGRES_PORT:-5433}:5432"
restart: unless-stopped
pgadmin:
container_name: pgadmin-${DOCKER_APP_NAME}
image: dpage/pgadmin4:latest
user: root
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@admin.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
PGADMIN_SERVER_JSON_FILE: '/pgadmin4/servers.json'
volumes:
- pgadmin_data:/var/lib/pgadmin
- ./docker/pgadmin/servers.json:/pgadmin4/servers.json:ro
- ./docker/pgadmin/pgpass:/pgadmin4/pgpass:ro
ports:
- "${PGADMIN_PORT:-5050}:80"
depends_on:
- db
restart: unless-stopped
entrypoint: >
/bin/sh -c "
mkdir -p /var/lib/pgadmin &&
cp /pgadmin4/pgpass /var/lib/pgadmin/pgpass &&
chmod 600 /var/lib/pgadmin/pgpass &&
chown 5050:5050 /var/lib/pgadmin/pgpass &&
/entrypoint.sh
"
volumes:
pg_data:
pgadmin_data:

View File

@@ -1,8 +1,8 @@
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

35
docker/.env.docker.local Normal file
View 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
View File

@@ -0,0 +1,2 @@
db:5432:inventory:root:root
db:5432:*:root:root

View 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"
}
}
}

View File

@@ -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

File diff suppressed because one or more lines are too long

42
fixtures/load.sh Executable file
View 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!"

View File

@@ -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
View 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"
```

View File

@@ -0,0 +1,201 @@
<?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('ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE');
$this->addSql('ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605');
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT "_ComposantConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT "_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('ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B');
$this->addSql('ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31');
$this->addSql('ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F');
$this->addSql('ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD');
$this->addSql('ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564');
$this->addSql('ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1');
$this->addSql('ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605');
$this->addSql('ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2');
$this->addSql('ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE');
$this->addSql('ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6');
$this->addSql('ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A');
$this->addSql('ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD');
$this->addSql('ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564');
$this->addSql('ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1');
$this->addSql('ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605');
$this->addSql('ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD');
$this->addSql('ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD');
$this->addSql('ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564');
$this->addSql('ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B');
$this->addSql('ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C');
$this->addSql('ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD');
$this->addSql('ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1');
$this->addSql('ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B');
$this->addSql('ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314');
$this->addSql('ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD');
$this->addSql('ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605');
$this->addSql('ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209');
$this->addSql('ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B');
$this->addSql('ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC');
$this->addSql('ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C');
$this->addSql('ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD');
$this->addSql('ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT "_MachineConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT "_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('ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B');
$this->addSql('ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31');
$this->addSql('DROP INDEX "ModelType_category_name_key"');
$this->addSql('DROP INDEX "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('ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6');
$this->addSql('ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605');
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT "_PieceConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT "_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('ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B');
$this->addSql('ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31');
$this->addSql('ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT "_ProductConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT "_ProductConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _productconstructeurs ALTER B TYPE VARCHAR(36)');
$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('ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B');
$this->addSql('ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31');
$this->addSql('DROP INDEX uniq_profiles_email');
$this->addSql('ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2');
$this->addSql('ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE');
$this->addSql('ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2');
$this->addSql('ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6');
$this->addSql('ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2');
$this->addSql('ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A');
}
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 FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_607601254AD0CF31');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT _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('ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"');
$this->addSql('ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT _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('ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"');
$this->addSql('ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT _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('ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"');
$this->addSql('ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT _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('ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"');
$this->addSql('ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43');
$this->addSql('ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7');
$this->addSql('ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B');
$this->addSql('ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6');
$this->addSql('ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61');
$this->addSql('ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F');
$this->addSql('ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73');
$this->addSql('ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7');
$this->addSql('ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B');
$this->addSql('ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3');
$this->addSql('ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8');
$this->addSql('ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B');
$this->addSql('ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6');
$this->addSql('ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F');
$this->addSql('ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F');
$this->addSql('ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73');
$this->addSql('ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7');
$this->addSql('ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6');
$this->addSql('ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F');
$this->addSql('ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF');
$this->addSql('ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3');
$this->addSql('ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF');
$this->addSql('ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F');
$this->addSql('ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73');
$this->addSql('ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF');
$this->addSql('ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"');
$this->addSql('ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"');
$this->addSql('ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF');
$this->addSql('ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF');
$this->addSql('ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6');
$this->addSql('ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F');
$this->addSql('ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3');
$this->addSql('ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F');
$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('ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8');
$this->addSql('ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7');
$this->addSql('ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B');
$this->addSql('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
$this->addSql('ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3');
$this->addSql('ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B');
$this->addSql('ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8');
$this->addSql('ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3');
$this->addSql('ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B');
$this->addSql('ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3');
}
}

70
public/.htaccess Normal file
View 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>

View 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;

View 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 $$;

View 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
View 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
View 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 "================================"

View 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";

View File

@@ -0,0 +1,287 @@
<?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),
];
}
}

View File

@@ -0,0 +1,119 @@
<?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);
}
}

View File

@@ -0,0 +1,76 @@
<?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
),
]);
}
}

View File

@@ -0,0 +1,756 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Composant;
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
foreach ($normalizedComponentLinks as &$link) {
$parentId = $link['parentComponentLinkId'] ?? null;
if ($parentId && isset($componentIndex[$parentId])) {
$componentIndex[$parentId]['childLinks'][] = &$link;
}
}
unset($link);
// Add pieces to components recursively
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
return [
'machine' => $this->normalizeMachine($machine),
'componentLinks' => array_values($componentIndex),
'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 ($name === null && $reference === null && $prix === null) {
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 ($value === null) {
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 ($value === null || $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));
}
}

View File

@@ -0,0 +1,90 @@
<?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]);
}
}

View 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(),
];
}
}

View 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!']);
}
}

View File

@@ -0,0 +1,110 @@
<?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|null $class = null,
): string {
return $this->getSQLResultCasing($platform, $columnName . '_' . $counter);
}
}

282
src/Entity/Composant.php Normal file
View 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: 500
)]
class Composant
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['composant:read'])]
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 = $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 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
View 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: 500
)]
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));
}
}

203
src/Entity/CustomField.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\CustomFieldRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomFieldRepository::class)]
#[ORM\Table(name: 'custom_fields')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
class CustomField
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
private string $name;
#[ORM\Column(type: Types::STRING, length: 50)]
private string $type;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
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)]
private ?array $options = null;
#[ORM\Column(type: Types::INTEGER, options: ['default' => 0], name: 'orderIndex')]
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\CustomFieldValueRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[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)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
private string $value;
#[ORM\ManyToOne(targetEntity: CustomField::class, inversedBy: 'customFieldValues')]
#[ORM\JoinColumn(name: 'customFieldId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
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')]
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

234
src/Entity/Document.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\DocumentRepository;
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]
class Document
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $name;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $filename;
#[ORM\Column(type: Types::TEXT)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $path;
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $mimeType;
#[ORM\Column(type: Types::INTEGER)]
#[Groups(['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')]
private ?Machine $machine = null;
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Composant $composant = null;
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Piece $piece = null;
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Product $product = null;
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Site $site = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

250
src/Entity/Machine.php Normal file
View File

@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\MachineRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource]
class Machine
{
#[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 $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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\MachineComponentLinkRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\MachinePieceLinkRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\MachineProductLinkRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

336
src/Entity/ModelType.php Normal file
View File

@@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
namespace App\Entity;
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'])]
#[ApiResource(
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
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 = $name;
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
View 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: 500
)]
class Piece
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['piece:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['piece:read'])]
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
View 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: 500
)]
class Product
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['product:read'])]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['product:read'])]
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
View 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();
}
}

263
src/Entity/Site.php Normal file
View File

@@ -0,0 +1,263 @@
<?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\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: 500
)]
class Site
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank]
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
View 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: 500
)]
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();
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\TypeMachineComponentRequirementRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\TypeMachinePieceRequirementRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\TypeMachineProductRequirementRepository;
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 ($this->id === null) {
$this->id = $this->generateCuid();
}
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
}
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;
}
}

View 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';
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\Piece;
use App\Repository\ProductRepository;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
/**
* Keep the legacy single product relation in sync with the new productIds array.
*/
final class PieceProductSyncSubscriber implements EventSubscriber
{
public function __construct(private readonly ProductRepository $productRepository)
{
}
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::preUpdate,
];
}
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Piece) {
return;
}
$this->syncPrimaryProduct($entity);
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Piece) {
return;
}
$this->syncPrimaryProduct($entity);
$em = $args->getObjectManager();
$meta = $em->getClassMetadata(Piece::class);
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
}
private function syncPrimaryProduct(Piece $piece): void
{
$productIds = $piece->getProductIds();
if ($productIds === []) {
// If no explicit list is provided, keep the legacy relation as-is.
return;
}
$primaryId = $productIds[0] ?? null;
if (!$primaryId) {
$piece->setProduct(null);
return;
}
$currentProductId = $piece->getProduct()?->getId();
if ($currentProductId === $primaryId) {
return;
}
$primaryProduct = $this->productRepository->find($primaryId);
$piece->setProduct($primaryProduct);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final class UniqueConstraintSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $this->findUniqueConstraintViolation($event->getThrowable());
if (!$exception) {
return;
}
$event->setResponse(new JsonResponse(
[
'success' => false,
'error' => 'nom duplique',
],
JsonResponse::HTTP_CONFLICT
));
}
private function findUniqueConstraintViolation(\Throwable $throwable): ?UniqueConstraintViolationException
{
for ($current = $throwable; $current !== null; $current = $current->getPrevious()) {
if ($current instanceof UniqueConstraintViolationException) {
return $current;
}
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Composant;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Composant>
*/
class ComposantRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Composant::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Constructeur;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Constructeur>
*/
class ConstructeurRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Constructeur::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\CustomField;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<CustomField>
*/
class CustomFieldRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomField::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\CustomFieldValue;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<CustomFieldValue>
*/
class CustomFieldValueRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, CustomFieldValue::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Document;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Document>
*/
class DocumentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Document::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MachineComponentLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MachineComponentLink>
*/
class MachineComponentLinkRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MachineComponentLink::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MachinePieceLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MachinePieceLink>
*/
class MachinePieceLinkRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MachinePieceLink::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MachineProductLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MachineProductLink>
*/
class MachineProductLinkRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MachineProductLink::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Machine;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Machine>
*/
class MachineRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Machine::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\ModelType;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ModelType>
*/
class ModelTypeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ModelType::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Piece;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Piece>
*/
class PieceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Piece::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Product>
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Profile;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<Profile>
*/
class ProfileRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Profile::class);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof Profile) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
}
$user->setPassword($newHashedPassword);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Site;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Site>
*/
class SiteRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Site::class);
}
// /**
// * @return Site[] Returns an array of Site objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('s.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Site
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TypeMachineComponentRequirement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TypeMachineComponentRequirement>
*/
class TypeMachineComponentRequirementRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TypeMachineComponentRequirement::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TypeMachinePieceRequirement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TypeMachinePieceRequirement>
*/
class TypeMachinePieceRequirementRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TypeMachinePieceRequirement::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TypeMachineProductRequirement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TypeMachineProductRequirement>
*/
class TypeMachineProductRequirementRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TypeMachineProductRequirement::class);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TypeMachine;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TypeMachine>
*/
class TypeMachineRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TypeMachine::class);
}
public function save(TypeMachine $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(TypeMachine $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}

View File

@@ -61,6 +61,18 @@
".php-cs-fixer.dist.php"
]
},
"lexik/jwt-authentication-bundle": {
"version": "3.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.5",
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
},
"files": [
"config/packages/lexik_jwt_authentication.yaml"
]
},
"nelmio/cors-bundle": {
"version": "2.6",
"recipe": {
@@ -204,5 +216,14 @@
"files": [
"config/packages/validator.yaml"
]
},
"vich/uploader-bundle": {
"version": "2.9",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.13",
"ref": "1b3064c2f6b255c2bc2f56461aaeb76b11e07e36"
}
}
}