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