Jonathan Scheiber

Mise à jour de Doctrine Migrations de 2.x à 3.0

Temps de lecture : 7 minutes 14 commentaires
Mise à jour de Doctrine Migrations de 2.x à 3.0

Suite à la mise à jour majeure de Doctrine Migrations et son bundle Symfony de la version 2.x à la 3.0, plusieurs problématiques se sont posées. Certaines faciles à régler, d'autres moins. Voyons comment régler tout cela.

Premiers pas après la mise à jour

Après la mise à jour de Doctrine Migrations et de son bundle Symfony, vous allez probablement vous retrouver avec l'erreur suivante (si vous n'avez pas mis à jour la configuration dans config/packages/doctrine_migrations.yaml) :

Unrecognized options "dir_name, namespace, table_name" under "doctrine_migrations". Available options are "all_or_nothing", "check_database_platform", "connection", "custom_template", "em", "factories", "migrations", "migrations_paths", "organize_migrations", "services", "storage".

Pour corriger cela, suivez le guide d'upgrade présent dans le repository du bundle.

Synchronisation de la structure de la table de migrations

Ensuite, en voulant par exemple vérifier le diff (avec bin/console doctrine:migrations:diff), vous allez probablement voir l'erreur suivante :

The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.

La structure de la table a changé pendant l'upgrade vers la 3.0 : maintenant, au lieu de stocker uniquement le numéro de version de la migration, ça va renseigner le FQCN (namespace + nom de la classe) des migrations exécutées. Ce qui veut dire qu'au lieu de voir par exemple 20200603064733 dans la table de migration, vous verrez DoctrineMigrations\Version20200603064733.

Heureusement, le message d'erreur est plutôt clair : il faut simplement lancer la commande bin/console doctrine:migrations:sync-metadata-storage.

Si la commande ne fonctionne pas, c'est que vous avez probablement mal réglé (ou pas du tout) la version du SGBD dans la configuration de Doctrine (paramètre server_version). Par exemple dans mon cas, je dois indiquer mariadb-10.3.22. Après réglage de la version, la commande passe bien.

Autres problèmes rencontrés

Attention si vous n'aviez pas indiqué un nom de table dans la configuration des migrations : dans certains cas, ma table se nommait migration_versions, mais dans la configuration par défaut ça a apparemment changé en doctrine_migration_versions à un moment donné. Donc si votre table s'appelle migration_versions et que rien n'est indiqué dans votre fichier de configuration, n'oubliez pas de configurer cela.

Si vous avez le message d'erreur suivant :

An exception occurred while executing 'ALTER TABLE migration_versions ADD execution_time INT DEFAULT NULL, CHANGE version version VARCHAR(1024) NOT NULL, CHANGE executed_at executed_at DATETIME DEFAULT NULL':  
  SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 3072 bytes

Vous devrez modifier la valeur de version_column_length dans la configuration par 191 (voici la pull request en question).

Autre détail important : la commande migrate affiche maintenant une erreur par défaut si aucune migration ne doit s'exécuter (et renvoie donc un code différent de 0, ce qui peut interrompre votre déploiement par exemple). La solution : passer l'option allow-no-migration, afin que la commande affiche plutôt une alerte de type warning (et renvoie un code 0), ce qui donne : bin/console doctrine:migrations:migrate --allow-no-migration

Mise à jour importante : ceci a été corrigé avec la version 3.0.1 :)

Le cas de plusieurs bases de données

Dans un gros projet, j'ai une gestion de 9 bases de données différentes. Le problème, c'est que les options em et connection ont disparu des différentes commandes de migration. Ces options servaient jusqu'à la version 2.x à spécifier sur quelle connexion/quel Entity Manager lancer les commandes de migration.

Après plusieurs recherches, je me rends compte que le plus simple va être l'utilisation du système de factories.

Vu que j'avais déjà créé dans le projet en question une série de commandes proxy, je les ai retravaillées afin de pouvoir gérer ça facilement.

A noter que j'ai, pour chaque Entity Manager, un fichier de configuration pour les migrations, stocké dans config/doctrine_migrations/. Par exemple pour l'EM gps (qui va se trouver dans config/doctrine_migrations/gps.yaml) :

table_storage:
    table_name: _migration_versions
migrations_paths:
    'Application\Migrations\Gps': 'database/migrations/gps'
organize_migrations: year

Voici un exemple de proxy pour la commande migrate :

<?php

declare(strict_types=1);

namespace App\Command\Migrations;

use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
use Doctrine\Migrations\Configuration\Migration\YamlFile;
use Doctrine\Migrations\DependencyFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

final class MigrateCommand extends Command
{
    private ManagerRegistry $registry;

    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct();
        $this->registry = $registry;
    }

    protected function configure(): void
    {
        $this
            ->setName('app:migrations:migrate')
            ->setDescription('Proxy to launch doctrine:migrations:migrate command as it would require a "configuration" option, and we can\'t define em/connection in config.')
            ->addArgument('em', InputArgument::REQUIRED, 'Name of the Entity Manager to handle.')
            ->addArgument('version', InputArgument::OPTIONAL, 'The version number (YYYYMMDDHHMMSS) or alias (first, prev, next, latest) to migrate to.', 'latest')
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the migration as a dry run.')
            ->addOption('query-time', null, InputOption::VALUE_NONE, 'Time all the queries individually.')
            ->addOption('allow-no-migration', null, InputOption::VALUE_NONE, 'Don\'t throw an exception if no migration is available (CI).')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $newInput = new ArrayInput([
            'version'               => $input->getArgument('version'),
            '--dry-run'             => $input->getOption('dry-run'),
            '--query-time'          => $input->getOption('query-time'),
            '--allow-no-migration'  => $input->getOption('allow-no-migration'),
        ]);
        $newInput->setInteractive($input->isInteractive());
        $otherCommand = new \Doctrine\Migrations\Tools\Console\Command\MigrateCommand($this->getDependencyFactory($input));
        $otherCommand->run($newInput, $output);

        return 0;
    }

    private function getDependencyFactory(InputInterface $input): DependencyFactory
    {
        $em = $this->registry->getManager($input->getArgument('em'));
        $config = new YamlFile(__DIR__ . '/../../../config/doctrine_migrations/' . $input->getArgument('em') . '.yaml');

        return DependencyFactory::fromEntityManager($config, new ExistingEntityManager($em));
    }
}

Je définis tout d'abord dans la configuration de la commande un argument essentiel : le nom de l'Entity Manager. Ensuite, je redéfinis les arguments et options de la commande de base qui m'intéressent.

Dans l'exécution de la commande, je vais définir les arguments/options à passer, puis je vais créer une nouvelle instance de la commande originale en lui passant le bon Entity Manager. Et enfin, je lance la commande.

En partant sur cet exemple, pour lancer les migrations de l'EM gps, je vais lancer la commande suivante : bin/console app:migrations:migrate gps --allow-no-migration

Et voilà, c'est enfin terminé ! :) Si vous avez des questions, remarques, suggestions ... N'hésitez pas à poster un commentaire ci-dessous :)

Commentaires

Posté par Rimi le 23/06/2020 à 10:27.
Thank you so much, very helpful article.
The still remaining one for me is factories. I am still new in symfony, I will give it a try and will let you know.

Thank you again!
Posté par Jenni le 23/06/2020 à 15:51.
Merci !

J'ai eu cette erreur 'Unrecognized options', grâce à ton article en 2 sec, j'ai résolu le problème de mise à jour de Doctrine.
Posté par Wolf le 22/07/2020 à 14:29.
Merci,
Sujet très bien abordé et mon problème a été résolu !
Posté par Fabrizio le 31/08/2020 à 17:34.
Hi Jonathan,
with Symfony 5.1.3 and two connection and two entity manager (example: default and booking_manager), how do i launch the command
php bin/console make:entity
using the entity manager booking_manager?

Best regards
Posté par jmsche le 06/09/2020 à 10:26.
Hi Fabrizio, the make:entity command does not need to know about your entity managers as Doctrine identifies the managers according to the namespace of your entities.
Posté par cBlondel le 24/11/2020 à 02:05.
Bonjour Jonathan,
Merci pour cette article !
Est-ce que vous utilisez la commande bin/console doctrine:migrations:diff avec des options spécifiques (--configuration, --db-configuration...) ou par l'intermédiaire d'une autre Commande Symfony ? De mon côté, cette commande ne génère pas mes migrations par entity manager. Elle regroupe le différentiel de l'ensemble de mes entités, sans tenir compte de leur appartenance à un entity manager.
Auriez-vous une idée ou des articles à me conseiller pour m'aider à avancer sur ce sujet ?
Merci pour votre aide.
Posté par jmsche le 24/11/2020 à 02:25.
Bonjour cBlondel,
Comme expliqué dans l'article, les commandes proxy créées manuellement vont charger un fichier de configuration spécifique ainsi que l'EntityManager lié, ce qui va normalement générer le diff pour les entités de l'EntityManager ciblé.
Le problème actuel de la v3.0 par rapport à la v2.x c'est qu'on ne peut pas cibler l'EntityManager ou la Connection qu'on veut via une option de la commande. Une PR est en cours sur le bundle doctrine/doctrine-migrations-bundle mais malheureusement pas encore fusionnée.
Posté par cBlondel le 24/11/2020 à 09:11.
Bonjour jmsche,
Merci pour votre réponse. En effet, j'ai vu que la version 3.1 de DoctrineMigrationsBundle permettra de renseigner de nouveau un EntityManager.
Pour résumer, dès que l'on souhaite communiquer l'EntityManager à une commande doctrine, on créé un proxy de cette commande (un proxy pour la commande doctrine:migrations:migrate, et un autre pour la commande doctrine:migrations:diff) ?
Merci
Posté par jmsche le 24/11/2020 à 09:29.
C'est bien cela :)
La commande proxy permet également de spécifier un fichier de configuration, qui permet notamment de spécifier le chemin de stockage des migrations en fonction de l'EntityManager, ce qui est plus propre à mon goût :)
Posté par Ludi le 20/12/2020 à 11:17.
Thanks a lot for your post.
Can you please provide the other commands as well?
e.g. app:migrations:diff
Posté par jmsche le 20/12/2020 à 13:40.
Hi Ludi,
The commands are pretty straight forward to write.
I wrote this gist a while ago, maybe this will suit your needs? https://gist.github.com/jmsche/2baf98b35eac936b0f6e77378c56f934
Posté par MushuLeDragon le 17/03/2021 à 12:07.
Bonjour,
Merci pour ce partage, ca correspond exactement à ma problématique.

Mais je voudrai savoir comment on peut faire pour la génération de fichiers de migration (`php bin/console make:migration`), sachant que actuellement je dois changer manuellement le fichier `doctrine_migrations.yaml` pour que la base de données corresponde, sachant que pour les migrations via doctrine je n'ai plus à le faire manuellement.

project
| - migration
| | - db1
| | - Version999998.php
| | - Version999999.php
| | - db2
| | - Version666666.php
| - public
| - src

Le but serait de n'avoir plus qu'une commande custom pour générer mes migrations et juste spécifier en argument la base de données associée (et donc les bonnes entités).

Merci
Posté par jmsche le 17/03/2021 à 13:26.
Bonjour MushuLeDragon,

Pour la génération de migrations, il suffit de créer un proxy pour la commande diff présente dans Doctrine Migrations.
Posté par MushuLeDragon le 17/03/2021 à 13:48.
Ohlalalala !
Je ne savais pas que d:m:diff faisait la même chose que make:migration !!

Merci infiniement

Ajouter un commentaire