From 8cfcb41a39e629a7ef4681680f32069a7b747d98 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 10 Apr 2026 16:57:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(conversion)=20:=20commande=20CLI=20pour=20?= =?UTF-8?q?convertir=20la=20cat=C3=A9gorie=20Moteur=20de=20PIECE=20vers=20?= =?UTF-8?q?COMPONENT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migre les 18 pièces en composants, transfère documents, custom fields, slots et skeleton requirements dans une transaction. Supporte --dry-run. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ConvertMoteurPieceToComponentCommand.php | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 src/Command/ConvertMoteurPieceToComponentCommand.php diff --git a/src/Command/ConvertMoteurPieceToComponentCommand.php b/src/Command/ConvertMoteurPieceToComponentCommand.php new file mode 100644 index 0000000..5bc527c --- /dev/null +++ b/src/Command/ConvertMoteurPieceToComponentCommand.php @@ -0,0 +1,320 @@ +addOption('dry-run', null, InputOption::VALUE_NONE, 'Affiche les actions sans les exécuter'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $dryRun = $input->getOption('dry-run'); + + $io->title('Conversion catégorie "Moteur" : PIECE → COMPONENT'); + + // ── 1. Vérifications ────────────────────────────────────────────── + + $modelType = $this->connection->fetchAssociative( + 'SELECT id, name, code, category FROM model_types WHERE id = :id', + ['id' => self::MODEL_TYPE_ID], + ); + + if (!$modelType) { + $io->error('ModelType "Moteur" introuvable (id: '.self::MODEL_TYPE_ID.')'); + + return Command::FAILURE; + } + + if ('PIECE' !== $modelType['category']) { + $io->error(sprintf('Le ModelType "Moteur" est déjà de catégorie %s — rien à faire.', $modelType['category'])); + + return Command::FAILURE; + } + + $pieces = $this->connection->fetchAllAssociative( + 'SELECT id, name, reference FROM pieces WHERE typepieceid = :id ORDER BY name', + ['id' => self::MODEL_TYPE_ID], + ); + + $pieceCount = count($pieces); + $io->info(sprintf('Pièces à convertir : %d', $pieceCount)); + + if ($pieceCount > 0) { + $io->table( + ['ID', 'Nom', 'Référence'], + array_map(fn (array $p) => [$p['id'], $p['name'], $p['reference'] ?? '—'], $pieces), + ); + } + + // Check blockers + $blockers = []; + + $machineLinked = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM machine_piece_links mpl + JOIN pieces p ON mpl.pieceid = p.id + WHERE p.typepieceid = :id', + ['id' => self::MODEL_TYPE_ID], + ); + + if ($machineLinked > 0) { + $blockers[] = sprintf('%d pièce(s) liée(s) à des machines — conversion impossible.', $machineLinked); + } + + $nameCollisions = $this->connection->fetchFirstColumn( + 'SELECT p.name FROM pieces p + WHERE p.typepieceid = :id + AND p.name IN (SELECT c.name FROM composants c)', + ['id' => self::MODEL_TYPE_ID], + ); + + if ([] !== $nameCollisions) { + $blockers[] = sprintf('Collision de noms avec des composants existants : %s', implode(', ', $nameCollisions)); + } + + $categoryCollision = (int) $this->connection->fetchOne( + "SELECT COUNT(*) FROM model_types WHERE category = 'COMPONENT' AND name = :name AND id != :id", + ['name' => $modelType['name'], 'id' => self::MODEL_TYPE_ID], + ); + + if ($categoryCollision > 0) { + $blockers[] = sprintf('Un ModelType composant « %s » existe déjà.', $modelType['name']); + } + + if ([] !== $blockers) { + $io->error($blockers); + + return Command::FAILURE; + } + + // Summary of related data + $relatedCounts = $this->countRelatedData(); + $io->section('Données liées à migrer'); + $io->table( + ['Table', 'Nombre'], + array_map(fn (string $k, int $v) => [$k, $v], array_keys($relatedCounts), array_values($relatedCounts)), + ); + + if ($dryRun) { + $io->warning('Mode dry-run : aucune modification effectuée.'); + + return Command::SUCCESS; + } + + // ── 2. Exécution ────────────────────────────────────────────────── + + $this->connection->beginTransaction(); + + try { + $now = new DateTimeImmutable()->format('Y-m-d H:i:s'); + + // 2a. Copier les pièces dans composants + $converted = $this->connection->executeStatement( + 'INSERT INTO composants (id, name, reference, referenceauto, description, prix, typecomposantid, productid, version, createdat, updatedat) + SELECT id, name, reference, referenceauto, description, prix, typepieceid, productid, version, createdat, :now + FROM pieces + WHERE typepieceid = :id', + ['id' => self::MODEL_TYPE_ID, 'now' => $now], + ); + $io->text(sprintf('✓ %d pièce(s) copiée(s) dans composants', $converted)); + + // 2b. Transférer les documents + $docs = $this->connection->executeStatement( + 'UPDATE documents SET composantid = pieceid, pieceid = NULL + WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d document(s) transféré(s)', $docs)); + + // 2c. Transférer les custom_field_values + $cfv = $this->connection->executeStatement( + 'UPDATE custom_field_values SET composantid = pieceid, pieceid = NULL + WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d valeur(s) de champs perso transférée(s)', $cfv)); + + // 2d. Transférer les custom_fields (définitions) + $cf = $this->connection->executeStatement( + 'UPDATE custom_fields SET typecomposantid = typepieceid, typepieceid = NULL + WHERE typepieceid = :id', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d définition(s) de champs perso transférée(s)', $cf)); + + // 2e. Transférer les constructeur links + $ctorLinks = $this->connection->executeStatement( + "INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat) + SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24), + pcl.pieceid, pcl.constructeurid, pcl.supplierreference, pcl.createdat, :now + FROM piece_constructeur_links pcl + WHERE pcl.pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)", + ['id' => self::MODEL_TYPE_ID, 'now' => $now], + ); + + if ($ctorLinks > 0) { + $this->connection->executeStatement( + 'DELETE FROM piece_constructeur_links + WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => self::MODEL_TYPE_ID], + ); + } + $io->text(sprintf('✓ %d lien(s) constructeur transféré(s)', $ctorLinks)); + + // 2f. Convertir composant_piece_slots → composant_subcomponent_slots + $slots = $this->connection->executeStatement( + "INSERT INTO composant_subcomponent_slots (id, composantid, alias, familycode, typecomposantid, selectedcomposantid, position, createdat, updatedat) + SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24), + cps.composantid, + COALESCE(sp.name, 'Moteur'), + 'moteur', + cps.typepieceid, + cps.selectedpieceid, + cps.position, + cps.createdat, + :now + FROM composant_piece_slots cps + LEFT JOIN pieces sp ON sp.id = cps.selectedpieceid + WHERE cps.typepieceid = :id", + ['id' => self::MODEL_TYPE_ID, 'now' => $now], + ); + + $this->connection->executeStatement( + 'DELETE FROM composant_piece_slots WHERE typepieceid = :id', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d slot(s) pièce convertis en slots sous-composant', $slots)); + + // 2g. Convertir skeleton_piece_requirements → skeleton_subcomponent_requirements + $skelReqs = $this->connection->executeStatement( + "INSERT INTO skeleton_subcomponent_requirements (id, modeltypeid, alias, familycode, typecomposantid, position, createdat, updatedat) + SELECT 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24), + spr.modeltypeid, + 'Moteur', + 'moteur', + spr.typepieceid, + spr.position, + spr.createdat, + :now + FROM skeleton_piece_requirements spr + WHERE spr.typepieceid = :id", + ['id' => self::MODEL_TYPE_ID, 'now' => $now], + ); + + $this->connection->executeStatement( + 'DELETE FROM skeleton_piece_requirements WHERE typepieceid = :id', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d skeleton requirement(s) convertis', $skelReqs)); + + // 2h. Mettre à jour audit_logs entity_type + $auditUpdated = $this->connection->executeStatement( + "UPDATE audit_logs SET entitytype = 'composant' + WHERE entitytype = 'piece' + AND entityid IN (SELECT id FROM pieces WHERE typepieceid = :id)", + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d audit log(s) mis à jour', $auditUpdated)); + + // 2i. Mettre à jour comments entity_type + $commentsUpdated = $this->connection->executeStatement( + "UPDATE comments SET entity_type = 'composant' + WHERE entity_type = 'piece' + AND entity_id IN (SELECT id FROM pieces WHERE typepieceid = :id)", + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d commentaire(s) mis à jour', $commentsUpdated)); + + // 2j. Supprimer les pièces originales + $deleted = $this->connection->executeStatement( + 'DELETE FROM pieces WHERE typepieceid = :id', + ['id' => self::MODEL_TYPE_ID], + ); + $io->text(sprintf('✓ %d pièce(s) supprimée(s)', $deleted)); + + // 2k. Changer la catégorie du ModelType + $this->connection->executeStatement( + "UPDATE model_types SET category = 'COMPONENT', updatedat = :now WHERE id = :id", + ['id' => self::MODEL_TYPE_ID, 'now' => $now], + ); + $io->text('✓ ModelType "Moteur" passé en COMPONENT'); + + $this->connection->commit(); + + $io->success(sprintf('Conversion terminée : %d pièces → composants.', $converted)); + + return Command::SUCCESS; + } catch (Throwable $e) { + $this->connection->rollBack(); + $io->error('Erreur — rollback effectué : '.$e->getMessage()); + + return Command::FAILURE; + } + } + + /** + * @return array + */ + private function countRelatedData(): array + { + $id = self::MODEL_TYPE_ID; + + return [ + 'pieces' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM pieces WHERE typepieceid = :id', + ['id' => $id], + ), + 'documents' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM documents WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $id], + ), + 'custom_field_values' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM custom_field_values WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $id], + ), + 'custom_fields' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM custom_fields WHERE typepieceid = :id', + ['id' => $id], + ), + 'piece_constructeur_links' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM piece_constructeur_links WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $id], + ), + 'composant_piece_slots (→ subcomponent_slots)' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM composant_piece_slots WHERE typepieceid = :id', + ['id' => $id], + ), + 'skeleton_piece_requirements (→ subcomponent_reqs)' => (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM skeleton_piece_requirements WHERE typepieceid = :id', + ['id' => $id], + ), + ]; + } +}