Compare commits

..

8 Commits

Author SHA1 Message Date
8d83076be0 chore(release) : v1.2.0 2026-01-29 19:55:29 +01:00
Matthieu
997a3ae822 chore(frontend): update submodule to history UI 2026-01-25 21:22:24 +01:00
Matthieu
034c193e4b feat(audit): add history tracking and bump version to 1.1.2 2026-01-25 21:19:42 +01:00
Matthieu
4acc8d1c01 chore(release) : v1.1.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:45 +01:00
Matthieu
49ff15f18d fix : correct commit message format in release script
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:08 +01:00
Matthieu
7a02617d48 chore : add qpdf to Docker image for PDF compression
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:06:08 +01:00
Matthieu
e52eef0491 docs : add PDF compression documentation to README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:03:36 +01:00
Matthieu
a5118305d3 feat : automatic PDF compression on upload
- Add PdfCompressorService for lossless compression with qpdf
- Add DocumentPdfCompressorListener for automatic compression on persist/update
- Add app:compress-pdf command for batch compression of existing PDFs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:02:26 +01:00
20 changed files with 2493 additions and 126 deletions

View File

@@ -44,6 +44,35 @@ make dev-nuxt
```
Le front sera accessible sur http://localhost:3000
## Compression automatique des PDFs
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
### Prérequis
```bash
# Installation de qpdf (outil système)
sudo apt install qpdf
# Ou dans Docker
docker exec -it php-inventory-apache apt update && apt install -y qpdf
```
### Fonctionnement
- À chaque upload de PDF, le système compresse automatiquement le fichier
- Compression lossless (sans perte de qualité)
- Le PDF est compressé uniquement si la taille diminue
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
### Compresser les PDFs existants
Pour compresser tous les PDFs déjà en base :
```bash
# Voir ce qui serait compressé (dry-run)
php bin/console app:compress-pdf --dry-run
# Compresser tous les PDFs
php bin/console app:compress-pdf
```
## Commandes utiles
Pour restart le container
```bash

View File

@@ -1 +1 @@
1.1.0
1.2.0

View File

@@ -1,6 +1,6 @@
api_platform:
title: Hello API Platform
version: 1.1.0
version: 1.2.0
defaults:
stateless: false
cache_headers:

View File

@@ -21,3 +21,15 @@ services:
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\EventSubscriber\ProductAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }
App\EventSubscriber\PieceAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }
App\EventSubscriber\ComposantAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }

View File

@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
wget \
git \
unzip \
qpdf \
&& docker-php-ext-install -j$(nproc) \
intl \
zip \

View File

@@ -20,165 +20,792 @@ final class Version20260125143939 extends AbstractMigration
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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199df92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _composantconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _composantconstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _composantconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_5b97d813e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_composantconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff6736d61') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7fff6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ffa1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff96428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ffa3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378cdf92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c4ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c40c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288a1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b0728896428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288fcf7805f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19a1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe197d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19bcced9e3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6294161596428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_629416157d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6294161532c54aaf') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('machine_product_links_machineid_idx') IS NOT NULL THEN
EXECUTE 'ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('machine_product_links_productid_idx') IS NOT NULL THEN
EXECUTE 'ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259357fdbff') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc322597d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259bcd7dad6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc3225987ceb33f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8dedfcf7805f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _machineconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _machineconstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _machineconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4f225b32e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_machineconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_category_name_key"');
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_code_key"');
$this->addSql('ALTER TABLE model_types ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE model_types ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE model_types ALTER componentSkeleton TYPE JSON');
$this->addSql('ALTER TABLE model_types ALTER pieceSkeleton TYPE JSON');
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d74724ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d7472a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _piececonstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _piececonstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _piececonstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_77fc120e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_piececonstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b3ba5a5a40c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _productconstructeurs ALTER B TYPE VARCHAR(36)');
// Clean orphaned relations before re-adding foreign keys.
$this->addSql('DELETE FROM _productconstructeurs WHERE A IS NULL OR B IS NULL');
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM products p WHERE p.id = pc.A)');
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM constructeurs c WHERE c.id = pc.B)');
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _productconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql('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');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_66f61802e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_productconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790df92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e4ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f98158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f9840c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A';
END IF;
END $$;
SQL
);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT 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 DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_607601254ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_60760125d3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e6a040cc4ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e6a040ccd3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e94732e54ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e94732e5d3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_cf7403fc4ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_cf7403fcd3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a319936799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199cc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff5c4a705f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff3c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff36799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c57b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378ccc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b072886973a4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b072883c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b0728836799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19c44b383c') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_629416153c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615f957d314') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc3225936799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259b590b209') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259a63ac5dc') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259937a1d7c') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded6973a4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE model_types ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR');
$this->addSql('ALTER TABLE model_types ALTER componentskeleton TYPE JSONB');
@@ -187,15 +814,78 @@ final class Version20260125143939 extends AbstractMigration
$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(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d7472169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d747236799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b3ba5a5a57b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B';
END IF;
END $$;
SQL
);
$this->addSql('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
$this->addSql('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');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_969587902f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790cc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f9857b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f982f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3';
END IF;
END $$;
SQL
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260125170000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add audit_logs table to store per-entity history entries.';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE audit_logs (
id VARCHAR(36) NOT NULL,
entityType VARCHAR(50) NOT NULL,
entityId VARCHAR(36) NOT NULL,
action VARCHAR(20) NOT NULL,
diff JSON DEFAULT NULL,
snapshot JSON DEFAULT NULL,
actorProfileId VARCHAR(36) DEFAULT NULL,
createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY(id)
)
SQL);
$this->addSql('CREATE INDEX idx_audit_entity ON audit_logs (entityType, entityId)');
$this->addSql('CREATE INDEX idx_audit_created_at ON audit_logs (createdAt)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE audit_logs');
}
}

View File

@@ -128,7 +128,7 @@ if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; th
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add -A
git commit -m "chore(release): prepare v$new_version"
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
@@ -168,7 +168,7 @@ sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
# ===========================================
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"
git commit -m "chore(release) : v$new_version"
# ===========================================
# ÉTAPE 6 : Tag principal

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\DocumentRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:compress-pdf',
description: 'Compress all PDF documents stored in database without quality loss',
)]
class CompressPdfCommand extends Command
{
public function __construct(
private readonly DocumentRepository $documentRepository,
private readonly EntityManagerInterface $em,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be compressed without actually doing it')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
// Check if qpdf is installed
exec('which qpdf', $qpdfPath, $returnCode);
if (0 !== $returnCode) {
$io->error('qpdf is not installed. Run: sudo apt install qpdf');
return Command::FAILURE;
}
$documents = $this->documentRepository->findBy(['mimeType' => 'application/pdf']);
if (empty($documents)) {
$io->info('No PDF documents found.');
return Command::SUCCESS;
}
$io->title('PDF Compression');
$io->text(sprintf('Found %d PDF documents', count($documents)));
$totalSaved = 0;
$compressed = 0;
foreach ($documents as $document) {
$base64Data = $document->getPath();
// Remove data URI prefix if present
if (str_contains($base64Data, ',')) {
$base64Data = explode(',', $base64Data, 2)[1];
}
$pdfContent = base64_decode($base64Data, true);
if (false === $pdfContent) {
$io->warning(sprintf('Failed to decode document: %s', $document->getName()));
continue;
}
$originalSize = strlen($pdfContent);
if ($dryRun) {
$io->text(sprintf(
' [DRY-RUN] Would compress: %s (%s)',
$document->getName(),
$this->formatBytes($originalSize)
));
continue;
}
// Create temp files
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
file_put_contents($tempInput, $pdfContent);
// Compress with qpdf (lossless)
$command = sprintf(
'qpdf --linearize --object-streams=generate %s %s 2>&1',
escapeshellarg($tempInput),
escapeshellarg($tempOutput)
);
exec($command, $cmdOutput, $returnCode);
if (0 !== $returnCode || !file_exists($tempOutput)) {
$io->warning(sprintf('Failed to compress: %s', $document->getName()));
@unlink($tempInput);
@unlink($tempOutput);
continue;
}
$compressedContent = file_get_contents($tempOutput);
$compressedSize = strlen($compressedContent);
// Only update if we actually saved space
if ($compressedSize < $originalSize) {
$saved = $originalSize - $compressedSize;
$totalSaved += $saved;
++$compressed;
// Rebuild base64 with data URI prefix
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
$document->setPath($newBase64);
$document->setSize($compressedSize);
$io->text(sprintf(
' ✓ %s: %s → %s (-%s, -%.1f%%)',
$document->getName(),
$this->formatBytes($originalSize),
$this->formatBytes($compressedSize),
$this->formatBytes($saved),
($saved / $originalSize) * 100
));
} else {
$io->text(sprintf(
' - %s: Already optimal (%s)',
$document->getName(),
$this->formatBytes($originalSize)
));
}
@unlink($tempInput);
@unlink($tempOutput);
}
if (!$dryRun && $compressed > 0) {
$this->em->flush();
$io->success(sprintf(
'Compressed %d/%d PDFs. Total space saved: %s',
$compressed,
count($documents),
$this->formatBytes($totalSaved)
));
} elseif ($dryRun) {
$io->info('Dry run completed. No changes made.');
} else {
$io->info('No PDFs needed compression.');
}
return Command::SUCCESS;
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
++$i;
}
return round($bytes, 2).' '.$units[$i];
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ComposantHistoryController
{
public function __construct(
private readonly ComposantRepository $components,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$component = $this->components->find($id);
if (!$component) {
return new JsonResponse(
['message' => 'Composant introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('composant', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\PieceRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PieceHistoryController
{
public function __construct(
private readonly PieceRepository $pieces,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$piece = $this->pieces->find($id);
if (!$piece) {
return new JsonResponse(
['message' => 'Pièce introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('piece', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ProductRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProductHistoryController
{
public function __construct(
private readonly ProductRepository $products,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$product = $this->products->find($id);
if (!$product) {
return new JsonResponse(
['message' => 'Produit introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('product', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

117
src/Entity/AuditLog.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\AuditLogRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AuditLogRepository::class)]
#[ORM\Table(name: 'audit_logs')]
#[ORM\Index(name: 'idx_audit_entity', columns: ['entityType', 'entityId'])]
#[ORM\Index(name: 'idx_audit_created_at', columns: ['createdAt'])]
#[ORM\HasLifecycleCallbacks]
class AuditLog
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 50)]
private string $entityType;
#[ORM\Column(type: Types::STRING, length: 36)]
private string $entityId;
#[ORM\Column(type: Types::STRING, length: 20)]
private string $action;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $diff = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $snapshot = null;
#[ORM\Column(type: Types::STRING, length: 36, nullable: true)]
private ?string $actorProfileId = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private DateTimeImmutable $createdAt;
public function __construct(
string $entityType,
string $entityId,
string $action,
?array $diff = null,
?array $snapshot = null,
?string $actorProfileId = null,
) {
$this->entityType = $entityType;
$this->entityId = $entityId;
$this->action = $action;
$this->diff = $diff;
$this->snapshot = $snapshot;
$this->actorProfileId = $actorProfileId;
}
#[ORM\PrePersist]
public function initializeAuditLog(): void
{
if (!isset($this->createdAt)) {
$this->createdAt = new DateTimeImmutable();
}
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
public function getId(): ?string
{
return $this->id;
}
public function getEntityType(): string
{
return $this->entityType;
}
public function getEntityId(): string
{
return $this->entityId;
}
public function getAction(): string
{
return $this->action;
}
public function getDiff(): ?array
{
return $this->diff;
}
public function getSnapshot(): ?array
{
return $this->snapshot;
}
public function getActorProfileId(): ?string
{
return $this->actorProfileId;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
private function generateCuid(): string
{
// Keep the same lightweight CUID-like strategy used across the project.
return 'cl'.substr(strtolower(base_convert(bin2hex(random_bytes(12)), 16, 36)), 0, 24);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\EventListener;
use App\Entity\Document;
use App\Service\PdfCompressorService;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;
#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: Document::class)]
#[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: Document::class)]
class DocumentPdfCompressorListener
{
public function __construct(
private readonly PdfCompressorService $pdfCompressor,
private readonly ?LoggerInterface $logger = null,
) {}
public function prePersist(Document $document): void
{
$this->compressIfPdf($document);
}
public function preUpdate(Document $document): void
{
$this->compressIfPdf($document);
}
private function compressIfPdf(Document $document): void
{
if ('application/pdf' !== $document->getMimeType()) {
return;
}
$result = $this->pdfCompressor->compressBase64Pdf($document->getPath());
if (null === $result) {
return;
}
$document->setPath($result['path']);
$document->setSize($result['size']);
$this->logger?->info('PDF compressed', [
'document' => $document->getName(),
'originalSize' => $result['originalSize'],
'compressedSize' => $result['size'],
'saved' => $result['saved'],
]);
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\Composant;
use App\Entity\ModelType;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
#[AsDoctrineListener(event: Events::onFlush)]
final class ComposantAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingComponents = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotComposant($entity);
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$componentId = (string) $entity->getId();
if ($componentId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
$pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
$pendingComponents[$componentId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$snapshot = $this->snapshotComposant($entity);
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
}
foreach ($pendingUpdates as $componentId => $diff) {
if ($diff === []) {
continue;
}
$component = $pendingComponents[$componentId] ?? null;
if (!$component instanceof Composant) {
continue;
}
$snapshot = $pendingSnapshots[$componentId] ?? $this->snapshotComposant($component);
$this->persistAuditLog($em, new AuditLog('composant', $componentId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Composant> $pendingComponents
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingComponents,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Composant) {
return;
}
$componentId = (string) $owner->getId();
if ($componentId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
$pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
$pendingComponents[$componentId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotComposant(Composant $component): array
{
return [
'id' => $component->getId(),
'name' => $component->getName(),
'reference' => $component->getReference(),
'prix' => $component->getPrix(),
'structure' => $component->getStructure(),
'typeComposant' => $this->normalizeValue($component->getTypeComposant()),
'product' => $this->normalizeValue($component->getProduct()),
'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
];
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Product) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'reference' => $value->getReference(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
#[AsDoctrineListener(event: Events::onFlush)]
final class PieceAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingPieces = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotPiece($entity);
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$pieceId = (string) $entity->getId();
if ($pieceId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
$pendingSnapshots[$pieceId] = $this->snapshotPiece($entity);
$pendingPieces[$pieceId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$snapshot = $this->snapshotPiece($entity);
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
}
foreach ($pendingUpdates as $pieceId => $diff) {
if ($diff === []) {
continue;
}
$piece = $pendingPieces[$pieceId] ?? null;
if (!$piece instanceof Piece) {
continue;
}
$snapshot = $pendingSnapshots[$pieceId] ?? $this->snapshotPiece($piece);
$this->persistAuditLog($em, new AuditLog('piece', $pieceId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Piece> $pendingPieces
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingPieces,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Piece) {
return;
}
$pieceId = (string) $owner->getId();
if ($pieceId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
$pendingSnapshots[$pieceId] = $this->snapshotPiece($owner);
$pendingPieces[$pieceId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotPiece(Piece $piece): array
{
return [
'id' => $piece->getId(),
'name' => $piece->getName(),
'reference' => $piece->getReference(),
'prix' => $piece->getPrix(),
'typePiece' => $this->normalizeValue($piece->getTypePiece()),
'product' => $this->normalizeValue($piece->getProduct()),
'productIds' => $piece->getProductIds(),
'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()),
];
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Product) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'reference' => $value->getReference(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\ModelType;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Record a lightweight, per-product audit trail.
*
* This MVP focuses on Product updates and captures:
* - scalar field changes (from Doctrine change sets)
* - constructeur collection changes (from collection updates)
*/
#[AsDoctrineListener(event: Events::onFlush)]
final class ProductAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingProducts = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotProduct($entity);
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$productId = (string) $entity->getId();
if ($productId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
$pendingSnapshots[$productId] = $this->snapshotProduct($entity);
$pendingProducts[$productId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$snapshot = $this->snapshotProduct($entity);
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
// Capture constructeur collection updates, which are not included in the change set.
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
}
foreach ($pendingUpdates as $productId => $diff) {
if ($diff === []) {
continue;
}
$product = $pendingProducts[$productId] ?? null;
if (!$product instanceof Product) {
continue;
}
$snapshot = $pendingSnapshots[$productId] ?? $this->snapshotProduct($product);
$this->persistAuditLog($em, new AuditLog('product', $productId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Product> $pendingProducts
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingProducts,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Product) {
return;
}
$productId = (string) $owner->getId();
if ($productId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
$pendingSnapshots[$productId] = $this->snapshotProduct($owner);
$pendingProducts[$productId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
// Ensure identifiers and timestamps are set even when persisting during onFlush.
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
// Skip noisy timestamps managed automatically.
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotProduct(Product $product): array
{
return [
'id' => $product->getId(),
'name' => $product->getName(),
'reference' => $product->getReference(),
'supplierPrice' => $product->getSupplierPrice(),
'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
];
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\AuditLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AuditLog>
*/
final class AuditLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AuditLog::class);
}
/**
* @return list<AuditLog>
*/
public function findEntityHistory(string $entityType, string $entityId, int $limit = 100): array
{
return $this->createQueryBuilder('a')
->andWhere('a.entityType = :entityType')
->andWhere('a.entityId = :entityId')
->setParameter('entityType', $entityType)
->setParameter('entityId', $entityId)
->orderBy('a.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Service;
class PdfCompressorService
{
public function compressBase64Pdf(string $base64Data): ?array
{
// Check if qpdf is available
exec('which qpdf', $qpdfPath, $returnCode);
if (0 !== $returnCode) {
return null;
}
// Remove data URI prefix if present
$originalBase64 = $base64Data;
if (str_contains($base64Data, ',')) {
$base64Data = explode(',', $base64Data, 2)[1];
}
$pdfContent = base64_decode($base64Data, true);
if (false === $pdfContent) {
return null;
}
$originalSize = strlen($pdfContent);
// Create temp files
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
file_put_contents($tempInput, $pdfContent);
// Compress with qpdf (lossless)
$command = sprintf(
'qpdf --linearize --object-streams=generate %s %s 2>&1',
escapeshellarg($tempInput),
escapeshellarg($tempOutput)
);
exec($command, $cmdOutput, $returnCode);
if (0 !== $returnCode || !file_exists($tempOutput)) {
@unlink($tempInput);
@unlink($tempOutput);
return null;
}
$compressedContent = file_get_contents($tempOutput);
$compressedSize = strlen($compressedContent);
@unlink($tempInput);
@unlink($tempOutput);
// Only return compressed version if it's smaller
if ($compressedSize >= $originalSize) {
return null;
}
// Rebuild with data URI prefix
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
return [
'path' => $newBase64,
'size' => $compressedSize,
'originalSize' => $originalSize,
'saved' => $originalSize - $compressedSize,
];
}
}