Jonathan Scheiber

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

Temps de lecture : 7 minutes 5 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.

Ajouter un commentaire