2026-01-05 22:41:40 +01:00
< ? php
/*
* This file is part of Part - DB ( https :// github . com / Part - DB / Part - DB - symfony ) .
*
* Copyright ( C ) 2019 - 2026 Jan Böhmer ( https :// github . com / jbtronics )
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*/
declare ( strict_types = 1 );
namespace App\Command\Migrations ;
use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper ;
2026-01-08 21:03:38 +01:00
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory ;
2026-01-05 23:25:53 +01:00
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform ;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform ;
2026-01-08 22:16:38 +01:00
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager ;
use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration ;
2026-01-08 21:03:38 +01:00
use Doctrine\ORM\EntityManager ;
2026-01-05 22:41:40 +01:00
use Doctrine\ORM\EntityManagerInterface ;
2026-01-08 22:16:38 +01:00
use Doctrine\Migrations\DependencyFactory ;
2026-01-05 22:41:40 +01:00
use Doctrine\ORM\Id\AssignedGenerator ;
use Doctrine\ORM\Mapping\ClassMetadata ;
use Symfony\Component\Console\Attribute\AsCommand ;
use Symfony\Component\Console\Command\Command ;
2026-01-08 21:03:38 +01:00
use Symfony\Component\Console\Input\InputArgument ;
2026-01-05 22:41:40 +01:00
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Output\OutputInterface ;
2026-01-05 23:14:40 +01:00
use Symfony\Component\Console\Style\SymfonyStyle ;
2026-01-08 22:22:07 +01:00
use Symfony\Component\DependencyInjection\Attribute\Autowire ;
2026-01-05 22:41:40 +01:00
2026-01-08 22:16:38 +01:00
#[AsCommand('partdb:migrations:convert-db-platform', 'Convert the database to a different platform')]
class DBPlatformConvertCommand extends Command
2026-01-05 22:41:40 +01:00
{
2026-01-08 21:03:38 +01:00
public function __construct (
private readonly EntityManagerInterface $targetEM ,
2026-01-05 22:41:40 +01:00
private readonly PKImportHelper $importHelper ,
2026-01-08 22:16:38 +01:00
private readonly DependencyFactory $dependencyFactory ,
2026-01-08 22:22:07 +01:00
#[Autowire('%kernel.project_dir%')]
private readonly string $kernelProjectDir ,
2026-01-05 22:41:40 +01:00
)
{
parent :: __construct ();
}
2026-01-08 21:03:38 +01:00
public function configure () : void
{
2026-01-08 22:22:47 +01:00
$this
-> setHelp ( 'This command allows you to migrate the database from one database platform to another (e.g. from MySQL to PostgreSQL).' )
-> addArgument ( 'url' , InputArgument :: REQUIRED , 'The database connection URL of the source database to migrate from' );
2026-01-08 21:03:38 +01:00
}
2026-01-05 22:41:40 +01:00
public function execute ( InputInterface $input , OutputInterface $output ) : int
{
2026-01-05 23:14:40 +01:00
$io = new SymfonyStyle ( $input , $output );
2026-01-08 22:16:38 +01:00
$sourceEM = $this -> getSourceEm ( $input -> getArgument ( 'url' ));
//Check that both databases are not using the same driver
if ( $sourceEM -> getConnection () -> getDatabasePlatform () :: class === $this -> targetEM -> getConnection () -> getDatabasePlatform () :: class ) {
$io -> error ( 'Source and target database are using the same database platform / driver. This command is only intended to migrate between different database platforms (e.g. from MySQL to PostgreSQL).' );
return 1 ;
}
$this -> ensureVersionUpToDate ( $sourceEM );
2026-01-08 21:03:38 +01:00
2026-01-08 22:22:07 +01:00
$io -> note ( 'This command is still in development. If you encounter any problems, please report them to the issue tracker on GitHub.' );
$io -> warning ( sprintf ( 'This command will delete all existing data in the target database "%s". Make sure that you have no important data in the database before you continue!' ,
$this -> targetEM -> getConnection () -> getDatabase () ? ? 'unknown'
));
$io -> ask ( 'Please type "DELETE ALL DATA" to continue.' , '' , function ( $answer ) {
if ( strtoupper ( $answer ) !== 'DELETE ALL DATA' ) {
throw new \RuntimeException ( 'You did not type "DELETE ALL DATA"!' );
}
return $answer ;
});
2026-01-05 22:41:40 +01:00
// Example migration logic (to be replaced with actual migration code)
2026-01-05 23:14:40 +01:00
$io -> info ( 'Starting database migration...' );
2026-01-05 22:41:40 +01:00
//Disable all event listeners on target EM to avoid unwanted side effects
$eventManager = $this -> targetEM -> getEventManager ();
foreach ( $eventManager -> getAllListeners () as $event => $listeners ) {
foreach ( $listeners as $listener ) {
$eventManager -> removeEventListener ( $event , $listener );
}
}
2026-01-05 23:14:40 +01:00
$io -> info ( 'Clear target database...' );
2026-01-05 22:41:40 +01:00
$this -> importHelper -> purgeDatabaseForImport ( $this -> targetEM , [ 'internal' , 'migration_versions' ]);
$metadata = $this -> targetEM -> getMetadataFactory () -> getAllMetadata ();
2026-01-05 23:14:40 +01:00
$io -> info ( 'Modifying entity metadata for migration...' );
2026-01-05 22:41:40 +01:00
//First we modify each entity metadata to have an persist cascade on all relations
foreach ( $metadata as $metadatum ) {
$entityClass = $metadatum -> getName ();
2026-01-05 23:14:40 +01:00
$io -> writeln ( 'Modifying cascade and ID settings for entity: ' . $entityClass , OutputInterface :: VERBOSITY_VERBOSE );
2026-01-05 22:41:40 +01:00
foreach ( $metadatum -> getAssociationNames () as $fieldName ) {
$mapping = $metadatum -> getAssociationMapping ( $fieldName );
$mapping -> cascade = array_unique ( array_merge ( $mapping -> cascade , [ 'persist' ]));
$metadatum -> setIdGeneratorType ( ClassMetadata :: GENERATOR_TYPE_NONE );
$metadatum -> setIdGenerator ( new AssignedGenerator ());
}
}
2026-01-05 23:14:40 +01:00
$io -> progressStart ( count ( $metadata ));
2026-01-08 22:16:38 +01:00
//Afterward we migrate all entities
2026-01-05 22:41:40 +01:00
foreach ( $metadata as $metadatum ) {
//skip all superclasses
if ( $metadatum -> isMappedSuperclass ) {
continue ;
}
$entityClass = $metadatum -> getName ();
2026-01-05 23:14:40 +01:00
$io -> note ( 'Migrating entity: ' . $entityClass );
2026-01-05 22:41:40 +01:00
2026-01-08 22:16:38 +01:00
$repo = $sourceEM -> getRepository ( $entityClass );
2026-01-05 22:41:40 +01:00
$items = $repo -> findAll ();
2026-01-05 23:14:40 +01:00
foreach ( $items as $index => $item ) {
2026-01-05 22:41:40 +01:00
$this -> targetEM -> persist ( $item );
}
$this -> targetEM -> flush ();
}
2026-01-05 23:14:40 +01:00
$io -> progressFinish ();
2026-01-05 22:41:40 +01:00
2026-01-05 23:25:53 +01:00
//Fix sequences / auto increment values on target database
$io -> info ( 'Fixing sequences / auto increment values on target database...' );
$this -> fixAutoIncrements ( $this -> targetEM );
2026-01-08 22:16:38 +01:00
$io -> success ( 'Database migration completed successfully.' );
2026-01-05 22:41:40 +01:00
2026-01-05 23:14:40 +01:00
if ( $io -> isVerbose ()) {
$io -> info ( 'Process took peak memory: ' . round ( memory_get_peak_usage ( true ) / 1024 / 1024 , 2 ) . ' MB' );
}
2026-01-05 22:41:40 +01:00
return Command :: SUCCESS ;
}
2026-01-05 23:25:53 +01:00
2026-01-08 22:16:38 +01:00
/**
* Construct a source EntityManager based on the given connection URL
* @ param string $url
* @ return EntityManagerInterface
*/
private function getSourceEm ( string $url ) : EntityManagerInterface
{
2026-01-08 22:22:07 +01:00
//Replace any %kernel.project_dir% placeholders
$url = str_replace ( '%kernel.project_dir%' , $this -> kernelProjectDir , $url );
2026-01-08 22:16:38 +01:00
$connectionFactory = new ConnectionFactory ();
$connection = $connectionFactory -> createConnection ([ 'url' => $url ]);
return new EntityManager ( $connection , $this -> targetEM -> getConfiguration ());
}
private function ensureVersionUpToDate ( EntityManagerInterface $sourceEM ) : void
{
//Ensure that target database is up to date
$migrationStatusCalculator = $this -> dependencyFactory -> getMigrationStatusCalculator ();
$newMigrations = $migrationStatusCalculator -> getNewMigrations ();
if ( count ( $newMigrations -> getItems ()) > 0 ) {
throw new \RuntimeException ( " Target database is not up to date. Please run all migrations (with doctrine:migrations:migrate) before starting the migration process. " );
}
$sourceDependencyLoader = DependencyFactory :: fromEntityManager ( new ExistingConfiguration ( $this -> dependencyFactory -> getConfiguration ()), new ExistingEntityManager ( $sourceEM ));
$sourceMigrationStatusCalculator = $sourceDependencyLoader -> getMigrationStatusCalculator ();
$sourceNewMigrations = $sourceMigrationStatusCalculator -> getNewMigrations ();
if ( count ( $sourceNewMigrations -> getItems ()) > 0 ) {
throw new \RuntimeException ( " Source database is not up to date. Please run all migrations (with doctrine:migrations:migrate) on the source database before starting the migration process. " );
}
}
2026-01-05 23:25:53 +01:00
private function fixAutoIncrements ( EntityManagerInterface $em ) : void
{
$connection = $em -> getConnection ();
$platform = $connection -> getDatabasePlatform ();
if ( $platform instanceof PostgreSQLPlatform ) {
$connection -> executeStatement (
2026-01-08 22:16:38 +01:00
//From: https://wiki.postgresql.org/wiki/Fixing_Sequences
2026-01-05 23:25:53 +01:00
<<< SQL
SELECT 'SELECT SETVAL(' ||
quote_literal ( quote_ident ( PGT . schemaname ) || '.' || quote_ident ( S . relname )) ||
', COALESCE(MAX(' || quote_ident ( C . attname ) || '), 1) ) FROM ' ||
quote_ident ( PGT . schemaname ) || '.' || quote_ident ( T . relname ) || ';'
FROM pg_class AS S ,
pg_depend AS D ,
pg_class AS T ,
pg_attribute AS C ,
pg_tables AS PGT
WHERE S . relkind = 'S'
AND S . oid = D . objid
AND D . refobjid = T . oid
AND D . refobjid = C . attrelid
AND D . refobjsubid = C . attnum
AND T . relname = PGT . tablename
ORDER BY S . relname ;
SQL );
};
}
2026-01-05 22:41:40 +01:00
}