diff --git a/composer.json b/composer.json
index 91c6d72f..143b30c7 100644
--- a/composer.json
+++ b/composer.json
@@ -41,7 +41,7 @@
"require": {
"php": "^8.0",
"doctrine/common": "^2.12 || ^3.0",
- "doctrine/dbal": "^2.6 || ^3.0",
+ "doctrine/dbal": "^3.2",
"doctrine/event-manager": "^1.0",
"doctrine/orm": "^2.10",
"doctrine/persistence": "^1.3 || ^2.0",
@@ -74,6 +74,9 @@
}
},
"config": {
+ "allow-plugins": {
+ "ergebnis/composer-normalize": true
+ },
"sort-packages": true
}
}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index ed4fb543..474fec88 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -135,3 +135,8 @@ parameters:
count: 1
path: tests/Fixtures/DemoEntityManager.php
+ -
+ message: "#^Call to function method_exists\\(\\) with Doctrine\\\\DBAL\\\\Connection and 'createSchemaManager' will always evaluate to true\\.$#"
+ count: 1
+ path: tests/Migration/IdToUuidMigrationTest.php
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4e88a2fa..5d0c5b23 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,6 +6,9 @@
+
+
+
diff --git a/src/Migration/IdToUuidMigration.php b/src/Migration/IdToUuidMigration.php
index 2c16def8..1a1afaf5 100644
--- a/src/Migration/IdToUuidMigration.php
+++ b/src/Migration/IdToUuidMigration.php
@@ -15,7 +15,7 @@
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Table;
-use Exception;
+use Doctrine\DBAL\Types\Types;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@@ -35,6 +35,12 @@
*/
final class IdToUuidMigration implements LoggerAwareInterface
{
+ public const UUID_FIELD = 'uuid';
+
+ public const UUID_TYPE = Types::STRING;
+
+ public const UUID_LENGTH = 36;
+
/**
* @var array
*/
@@ -57,7 +63,7 @@ final class IdToUuidMigration implements LoggerAwareInterface
private LoggerInterface $logger;
- public function __construct(Connection $connection, ?LoggerInterface $logger)
+ public function __construct(Connection $connection, ?LoggerInterface $logger = null)
{
$this->connection = $connection;
$this->schemaManager = method_exists($this->connection, 'createSchemaManager')
@@ -76,37 +82,20 @@ public function setLogger(LoggerInterface $logger): void
*/
public function migrate(string $tableName, string $idField = 'id', callable $callback = null): void
{
- $this->writeln(sprintf('Migrating %s.%s field to UUID...', $tableName, $idField));
+ $this->section(sprintf('Migrating %s.%s field to UUID', $tableName, $idField));
+
$this->prepare($tableName, $idField);
$this->addUuidFields();
$this->generateUuidsToReplaceIds();
$this->addThoseUuidsToTablesWithFK();
- if (null !== $callback) {
- $this->handleCallback($callback);
- }
+ $this->handleCallback($callback);
$this->deletePreviousFKs();
- $this->renameNewFKsToPreviousNames();
+ $this->deletePrimaryKeys();
+ $this->recreateIdFields();
+ $this->syncIdFields();
+ $this->dropTemporyForeignKeyUuidFields();
$this->dropIdPrimaryKeyAndSetUuidToPrimaryKey();
$this->restoreConstraintsAndIndexes();
- $this->writeln(sprintf('Successfully migrated %s.%s to UUID', $tableName, $idField));
- }
-
- private function writeln(string $message): void
- {
- $this->logger->notice($message, [
- 'migration' => $this,
- ]);
- }
-
- private function isForeignKeyNullable(Table $table, string $key): bool
- {
- foreach ($table->getColumns() as $column) {
- if ($column->getName() === $key) {
- return !$column->getNotnull();
- }
- }
-
- throw new RuntimeException('Unable to find '.$key.'in '.$table->getName());
}
private function prepare(string $tableName, string $idField): void
@@ -146,25 +135,40 @@ private function prepare(string $tableName, string $idField): void
}
if (\count($this->foreignKeys) > 0) {
- $this->writeln('-> Detected foreign keys:');
+ $this->section('Detected foreign keys:');
foreach ($this->foreignKeys as $meta) {
- $this->writeln(' * '.$meta['table'].'.'.$meta['key']);
+ $this->section(' * '.$meta['table'].'.'.$meta['key']);
}
return;
}
- $this->writeln('-> No foreign keys detected.');
+ $this->info('No foreign keys detected');
}
private function addUuidFields(): void
{
- $this->connection->executeQuery('ALTER TABLE '.$this->table.' ADD uuid VARCHAR(36) FIRST');
+ $this->section('Adding new "uuid" fields');
+
+ $schema = $this->schemaManager->createSchema();
+
+ $table = $schema->getTable($this->table);
+ $table->addColumn(self::UUID_FIELD, self::UUID_TYPE, [
+ 'length' => self::UUID_LENGTH,
+ 'notnull' => false,
+ ]);
foreach ($this->foreignKeys as $foreignKey) {
- $this->connection->executeQuery('ALTER TABLE '.$foreignKey['table'].' ADD '.$foreignKey['tmpKey'].' VARCHAR(36)');
+ $table = $schema->getTable($foreignKey['table']);
+ $table->addColumn($foreignKey['tmpKey'], self::UUID_TYPE, [
+ 'length' => self::UUID_LENGTH,
+ 'notnull' => false,
+ 'customSchemaOptions' => ['FIRST'],
+ ]);
}
+
+ $this->schemaManager->migrateSchema($schema);
}
private function generateUuidsToReplaceIds(): void
@@ -175,32 +179,20 @@ private function generateUuidsToReplaceIds(): void
return;
}
- $this->writeln('-> Generating '.\count($fetchs).' UUID(s)...');
+ $this->section('Generating '.\count($fetchs).' UUIDs');
foreach ($fetchs as $fetch) {
$id = $fetch[$this->idField];
$uuid = Uuid::v4()->toRfc4122();
$this->idToUuidMap[$id] = $uuid;
$this->connection->update($this->table, [
- 'uuid' => $uuid,
+ self::UUID_FIELD => $uuid,
], [
$this->idField => $id,
]);
}
}
- /**
- * @param callable(mixed $id, string $uuid): void $callback
- */
- private function handleCallback(callable $callback): void
- {
- $this->writeln('-> Executing callback');
-
- foreach ($this->idToUuidMap as $old => $new) {
- $callback($old, $new);
- }
- }
-
/**
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
@@ -210,7 +202,7 @@ private function addThoseUuidsToTablesWithFK(): void
return;
}
- $this->writeln('-> Adding UUIDs to tables with foreign keys...');
+ $this->section('Adding UUIDs to tables with foreign keys');
foreach ($this->foreignKeys as $foreignKey) {
$primaryKeys = array_map(static fn (Column $column) => $column->getName(), $foreignKey['primaryKey']);
@@ -223,7 +215,7 @@ private function addThoseUuidsToTablesWithFK(): void
continue;
}
- $this->writeln(' * Adding '.\count($fetchs).' UUIDs to "'.$foreignKey['table'].'.'.$foreignKey['key'].'"...');
+ $this->debug('Adding '.\count($fetchs).' UUIDs to "'.$foreignKey['table'].'.'.$foreignKey['key']);
foreach ($fetchs as $fetch) {
if (null === $fetch[$foreignKey['key']]) {
@@ -242,61 +234,179 @@ private function addThoseUuidsToTablesWithFK(): void
}
}
+ /**
+ * @param null|callable(mixed $id, string $uuid): void $callback
+ */
+ private function handleCallback(?callable $callback): void
+ {
+ if (null === $callback) {
+ return;
+ }
+
+ $this->section('Executing callback');
+
+ foreach ($this->idToUuidMap as $old => $new) {
+ $callback($old, $new);
+ }
+ }
+
private function deletePreviousFKs(): void
{
- $this->writeln('-> Deleting previous foreign keys...');
+ $this->section('Deleting previous foreign keys');
+
+ $schema = $this->schemaManager->createSchema();
foreach ($this->foreignKeys as $foreignKey) {
+ $table = $schema->getTable($foreignKey['table']);
+
+ $table->removeForeignKey($foreignKey['name']);
+ $table->dropColumn($foreignKey['key']);
+ }
+
+ $this->schemaManager->migrateSchema($schema);
+ }
+
+ private function deletePrimaryKeys(): void
+ {
+ $this->section('Deleting previous primary keys');
+
+ $schema = $this->schemaManager->createSchema();
+
+ foreach ($this->foreignKeys as $foreignKey) {
+ $table = $schema->getTable($foreignKey['table']);
+
if ([] !== $foreignKey['primaryKey']) {
- try {
- // drop primary key if not already dropped
- $this->connection->executeQuery('ALTER TABLE '.$foreignKey['table'].' DROP PRIMARY KEY');
- } catch (Exception) {
+ if ($table->hasPrimaryKey()) {
+ $table->dropPrimaryKey();
}
}
+ }
+
+ $this->schemaManager->migrateSchema($schema);
+ }
+
+ private function recreateIdFields(): void
+ {
+ $this->section('Recreate id fields');
+
+ $schema = $this->schemaManager->createSchema();
- $this->connection->executeQuery('ALTER TABLE '.$foreignKey['table'].' DROP FOREIGN KEY '.$foreignKey['name']);
- $this->connection->executeQuery('ALTER TABLE '.$foreignKey['table'].' DROP COLUMN '.$foreignKey['key']);
+ $table = $schema->getTable($this->table);
+ $table->dropColumn($this->idField);
+ $table->addColumn($this->idField, self::UUID_TYPE, [
+ 'length' => self::UUID_LENGTH,
+ 'notnull' => false,
+ 'customSchemaOptions' => ['FIRST'],
+ ]);
+
+ foreach ($this->foreignKeys as $foreignKey) {
+ $table = $schema->getTable($foreignKey['table']);
+
+ $table->dropColumn($foreignKey['key']);
+ $table->addColumn($foreignKey['key'], self::UUID_TYPE, [
+ 'length' => self::UUID_LENGTH,
+ 'notnull' => false,
+ ]);
}
+
+ $this->schemaManager->migrateSchema($schema);
}
- private function renameNewFKsToPreviousNames(): void
+ private function syncIdFields(): void
{
- $this->writeln('-> Renaming temporary foreign keys to previous foreign keys names...');
+ $this->section('Copy UUIDs to recreated ids fields');
+
+ $this->connection->executeQuery(sprintf('UPDATE %s SET %s = %s', $this->table, $this->idField, self::UUID_FIELD));
- foreach ($this->foreignKeys as $fk) {
- $this->connection->executeQuery('ALTER TABLE '.$fk['table'].' CHANGE '.$fk['tmpKey'].' '.$fk['key'].' VARCHAR(36) '.(true === $fk['nullable'] ? '' : 'NOT NULL '));
+ foreach ($this->foreignKeys as $foreignKey) {
+ $this->connection->executeQuery(sprintf('UPDATE %s SET %s = %s', $foreignKey['table'], $foreignKey['key'], $foreignKey['tmpKey']));
+ }
+ }
+
+ private function dropTemporyForeignKeyUuidFields(): void
+ {
+ $this->section('Drop temporary foreign key uuid fields');
+
+ $schema = $this->schemaManager->createSchema();
+
+ foreach ($this->foreignKeys as $foreignKey) {
+ $table = $schema->getTable($foreignKey['table']);
+
+ $table->dropColumn($foreignKey['tmpKey']);
}
+
+ $this->schemaManager->migrateSchema($schema);
}
private function dropIdPrimaryKeyAndSetUuidToPrimaryKey(): void
{
- $this->writeln('-> Creating the new primary key...');
+ $this->section('Creating the new primary key');
+
+ $schema = $this->schemaManager->createSchema();
+ $table = $schema->getTable($this->table);
- $this->connection->executeQuery('ALTER TABLE '.$this->table.' DROP PRIMARY KEY, DROP COLUMN '.$this->idField);
- $this->connection->executeQuery('ALTER TABLE '.$this->table.' CHANGE uuid '.$this->idField.' VARCHAR(36) NOT NULL');
- $this->connection->executeQuery('ALTER TABLE '.$this->table.' ADD PRIMARY KEY ('.$this->idField.')');
+ $table->dropPrimaryKey();
+ $table->dropColumn(self::UUID_FIELD);
+
+ $table->setPrimaryKey([$this->idField]);
+
+ $this->schemaManager->migrateSchema($schema);
}
private function restoreConstraintsAndIndexes(): void
{
+ $schema = $this->schemaManager->createSchema();
+
foreach ($this->foreignKeys as $foreignKey) {
+ $table = $schema->getTable($foreignKey['table']);
+
if ([] !== $foreignKey['primaryKey']) {
$primaryKeys = array_map(static fn (Column $column) => $column->getName(), $foreignKey['primaryKey']);
- try {
- // restore primary key if not already restored
- $this->connection->executeQuery('ALTER TABLE '.$foreignKey['table'].' ADD PRIMARY KEY ('.implode(',', $primaryKeys).')');
- } catch (Exception) {
+ if (!$table->hasPrimaryKey()) {
+ $table->setPrimaryKey($primaryKeys);
}
}
- $this->connection->executeQuery(
- 'ALTER TABLE '.$foreignKey['table'].' ADD CONSTRAINT '.$foreignKey['name'].' FOREIGN KEY ('.$foreignKey['key'].') REFERENCES '.$this->table.' ('.$this->idField.')'.
- (isset($foreignKey['onDelete']) ? ' ON DELETE '.$foreignKey['onDelete'] : '')
- );
+ $table->addForeignKeyConstraint($this->table, [$foreignKey['key']], [$this->idField], [
+ 'onDelete' => $foreignKey['onDelete'] ?? null,
+ ]);
+ $table->addIndex([$foreignKey['key']]);
+ }
+
+ $this->schemaManager->migrateSchema($schema);
+ }
+
+ private function section(string $message): void
+ {
+ $this->writeLn(sprintf('%s', $message));
+ }
+
+ private function info(string $message): void
+ {
+ $this->writeLn(sprintf('-> %s', $message));
+ }
+
+ private function debug(string $message): void
+ {
+ $this->writeLn(sprintf(' * %s', $message));
+ }
+
+ private function writeLn(string $message): void
+ {
+ $this->logger->notice($message, [
+ 'migration' => $this,
+ ]);
+ }
- $this->connection->executeQuery('CREATE INDEX '.str_replace('FK_', 'IDX_', $foreignKey['name']).' ON '.$foreignKey['table'].' ('.$foreignKey['key'].')');
+ private function isForeignKeyNullable(Table $table, string $key): bool
+ {
+ foreach ($table->getColumns() as $column) {
+ if ($column->getName() === $key) {
+ return !$column->getNotnull();
+ }
}
+
+ throw new RuntimeException('Unable to find '.$key.'in '.$table->getName());
}
}
diff --git a/tests/Bridge/Symfony/App/AppKernel.php b/tests/Bridge/Symfony/App/AppKernel.php
index cd1d11ab..48892313 100644
--- a/tests/Bridge/Symfony/App/AppKernel.php
+++ b/tests/Bridge/Symfony/App/AppKernel.php
@@ -68,12 +68,12 @@ protected function configureRoutes($routes): void
protected function configureContainer($container, $loader): void
{
if ($container instanceof ContainerConfigurator) {
- $container->import(__DIR__.'/config/config.yaml');
+ $container->import(__DIR__.'/config/config.php');
return;
}
- $loader->load(__DIR__.'/config/config.yaml');
+ $loader->load(__DIR__.'/config/config.php');
}
private function getBaseDir(): string
diff --git a/tests/Bridge/Symfony/App/config/config.php b/tests/Bridge/Symfony/App/config/config.php
new file mode 100644
index 00000000..2aadb99a
--- /dev/null
+++ b/tests/Bridge/Symfony/App/config/config.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+use Nucleos\Doctrine\Tests\Bridge\Symfony\App\Controller\SampleTestController;
+
+return static function (ContainerConfigurator $containerConfigurator): void {
+ $containerConfigurator->extension('framework', ['secret' => 'MySecret']);
+
+ $containerConfigurator->extension('framework', ['test' => true]);
+
+ $containerConfigurator->extension('doctrine', ['dbal' => ['url' => 'sqlite:///:memory:', 'logging' => false]]);
+
+ $services = $containerConfigurator->services();
+
+ $services->defaults()
+ ->autowire()
+ ->autoconfigure()
+ ;
+
+ $services
+ ->set(SampleTestController::class)
+ ->tag('controller.service_arguments')
+ ;
+};
diff --git a/tests/Bridge/Symfony/App/config/config.yaml b/tests/Bridge/Symfony/App/config/config.yaml
deleted file mode 100644
index 004d1367..00000000
--- a/tests/Bridge/Symfony/App/config/config.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-framework:
- secret: secret
-
-services:
- _defaults:
- autowire: true
- autoconfigure: true
-
- Nucleos\Doctrine\Tests\Bridge\Symfony\App\Controller\SampleTestController:
- tags:
- - controller.service_arguments
diff --git a/tests/Migration/IdToUuidMigrationTest.php b/tests/Migration/IdToUuidMigrationTest.php
new file mode 100644
index 00000000..987ba03d
--- /dev/null
+++ b/tests/Migration/IdToUuidMigrationTest.php
@@ -0,0 +1,163 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nucleos\Doctrine\Tests\Migration;
+
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\Persistence\AbstractManagerRegistry;
+use Nucleos\Doctrine\Migration\IdToUuidMigration;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+
+final class IdToUuidMigrationTest extends KernelTestCase
+{
+ public function testMigrate(): void
+ {
+ $kernel = self::createKernel();
+ $kernel->boot();
+
+ $connection = $this->getConnection();
+
+ $this->createSchema($connection);
+ $this->insertData($connection);
+
+ $migration = new IdToUuidMigration($connection);
+ $migration->migrate('category');
+
+ $this->verifyCategoryData($connection);
+ $this->verifyItemData($connection);
+ }
+
+ private function createCategory(Schema $schema): void
+ {
+ $table = $schema->createTable('category');
+ $table->addColumn('id', Types::INTEGER, [
+ 'autoincrement' => true,
+ ]);
+ $table->addColumn('parent_id', Types::INTEGER, [
+ 'notnull' => false,
+ ]);
+ $table->addColumn('name', Types::STRING, [
+ 'length' => 50,
+ ]);
+ $table->addForeignKeyConstraint('category', ['parent_id'], ['id'], [
+ 'onDelete' => 'SET NULL',
+ ]);
+ $table->addIndex(['parent_id']);
+ $table->setPrimaryKey(['id']);
+ }
+
+ private function createItem(Schema $schema): void
+ {
+ $table = $schema->createTable('item');
+ $table->addColumn('id', Types::INTEGER, [
+ 'autoincrement' => true,
+ ]);
+ $table->addColumn('category_id', Types::INTEGER);
+ $table->addColumn('name', Types::STRING, [
+ 'length' => 50,
+ ]);
+ $table->addForeignKeyConstraint('category', ['category_id'], ['id'], [
+ 'onDelete' => 'SET NULL',
+ ]);
+ $table->addIndex(['category_id']);
+ $table->setPrimaryKey(['id']);
+ }
+
+ private function getConnection(): Connection
+ {
+ $registry = self::getContainer()->get('doctrine');
+
+ \assert($registry instanceof AbstractManagerRegistry);
+
+ $connection = $registry->getConnection();
+
+ \assert($connection instanceof Connection);
+
+ return $connection;
+ }
+
+ private function createSchema(Connection $connection): void
+ {
+ $schemaManager = method_exists($connection, 'createSchemaManager')
+ ? $connection->createSchemaManager()
+ : $connection->getSchemaManager();
+ $schema = $schemaManager->createSchema();
+ $this->createCategory($schema);
+ $this->createItem($schema);
+ $schemaManager->migrateSchema($schema);
+
+ static::assertTrue($schemaManager->tablesExist('category'));
+ static::assertTrue($schemaManager->tablesExist('item'));
+ }
+
+ /**
+ * @throws \Doctrine\DBAL\Exception
+ */
+ private function insertData(Connection $connection): void
+ {
+ $connection->insert('category', [
+ 'id' => 1,
+ 'name' => 'Main',
+ 'parent_id' => null,
+ ]);
+ $connection->insert('category', [
+ 'id' => 2,
+ 'name' => 'Sub 1',
+ 'parent_id' => 1,
+ ]);
+ $connection->insert('category', [
+ 'id' => 3,
+ 'name' => 'Sub 2',
+ 'parent_id' => 1,
+ ]);
+ $connection->insert('category', [
+ 'id' => 4,
+ 'name' => 'Sub Sub',
+ 'parent_id' => 3,
+ ]);
+
+ $connection->insert('item', [
+ 'id' => 1,
+ 'name' => 'Item 1',
+ 'category_id' => 1,
+ ]);
+ $connection->insert('item', [
+ 'id' => 2,
+ 'name' => 'Item 2',
+ 'category_id' => 2,
+ ]);
+ $connection->insert('item', [
+ 'id' => 3,
+ 'name' => 'Item 3',
+ 'category_id' => 3,
+ ]);
+ }
+
+ private function verifyCategoryData(Connection $connection): void
+ {
+ $result = $connection->fetchAllAssociative('SELECT id, parent_id FROM category');
+ static::assertCount(4, $result);
+
+ foreach ($result as $data) {
+ static::assertTrue(36 === \strlen($data['id']));
+ }
+ }
+
+ private function verifyItemData(Connection $connection): void
+ {
+ $result = $connection->fetchAllAssociative('SELECT id, category_id FROM item');
+ static::assertCount(3, $result);
+
+ foreach ($result as $data) {
+ static::assertFalse(36 === \strlen($data['id']));
+ }
+ }
+}