Doctrine migrations et multiples bundles avec composer

  • doctrine2
  • symfony2
  • doctrineMigration
  • chill
  • bundles
  • composer
  • composer-php

Un de nos projets (Chill, un logiciel pour services sociaux) assemble plusieurs bundles Symfony2 dans une application. Il est également prévu que les utilisateurs puissent, selon leurs besoins, installer plusieurs bundles de leur choix, voire d'en créer de nouveaux. Les bundles peuvent avoir éventuellement besoin de la base de données: ils doivent alors être capables de créer des tables, de les manipuler, etc.

Pour gérer la création du schéma de la base de données, nous utilisons DoctrineMigrationsBundle, un composant qui s'intègre à l'application et permet de gérer les modifications du schéma. Les modifications sont enregistrées dans des classes de migrations, dont la structure doit être celle-ci :

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

# ici, 20100416130401 est un timestamp de l'heure 
# de création de la classe. Il l'identifie 
# de manière unique
class Version20100416130401 extends AbstractMigration
{
    public function up(Schema $schema)
    {}

    public function down(Schema $schema)
    {}
}

Les classes de migrations doivent être enregistrées dans un répertoire spécifique (app/DoctrineMigrations, par défaut).

Le problème

Notre application assemble différents bundles qui apportent leurs propres fichiers de migrations.

Or, si l'on peut éventuellement configurer le répertoire dans lequel le bundle DoctrineMigration s'attend à trouver les classes de migrations, est situé au niveau de l'application, et pas des bundles importés. Il est impossible, actuellement, de configurer plusieurs répertoires.

Notre solution

Nous utilisons composer pour gérer l'assemblage des bundles : nous nous sommes appuyés sur les scripts du gestionnaire de paquets pour s'assurer que le répertoire de l'application `app/DoctrineMigrations` soit synchronisé avec les fichiers de migrations apportées par les bundles.

Concrètement, après chaque installation et/ou mise à jour d'un package, un script :

  • vérifie si le nouveau venu apporte de nouveaux fichiers de migrations, ou si ceux-ci ont été modifiés (la comparaison utilise un timestamp md5) ;
  • si c'est le cas, le fichier est recopié dans le répertorie app/DoctrineMigrations; si un fichier a été modifié, une confirmation est demandée à l'utilisateur ;

Le code

La classe qui effectue les migrations peut être inspectée sur notre dépot. La partie la moins documentée consiste à

  • récupérer la liste des packages installés ;
  • trouver le chemin d'installation du package ;

Le code de ces parties a été isolé :

// le fichier est enregistré dans app/Composer/Migrations.php

namespace Chill\Composer;

use Composer\Script\CommandEvent;
use Symfony\Component\Filesystem\Filesystem;
use Composer\IO\IOInterface;


class Migrations
{
    
    public static function synchronizeMigrations(CommandEvent $event)
    {
        # récupère la liste des packages
        $packages = $event->getComposer()->getRepositoryManager()
              ->getLocalRepository()->getPackages();

        # l'installation manager permet de deviner le 
        # chemin d'installation du package
        $installer = $event->getComposer()->getInstallationManager();
        
        # ...

        foreach($packages as $package) {
            //pour trouver le chemin d'installation : 
            $installPath = $installer->getInstallPath($package);
            
            # ...
            
        }
        
    }
    
}

Les instructions dans composer.json

Le fichier composer.json doit contenir les instructions suivantes pour exécuter le script correctement :

  • instructions pour permettre de charger la classe "Migrations" enregistrée dans le répertoire app/Composer :
"autoload": {
   "psr-4": {"Chill\\Composer\\" : "app/Composer/"}
}
  • instructions pour exécuter les scripts après chaque commande composer update et composer install :
"scripts": {
   "post-install-cmd": [
      "Chill\\Composer\\Migrations::synchronizeMigrations"
   ],
   "post-update-cmd": [
      "Chill\\Composer\\Migrations::synchronizeMigrations"
   ]
}

Choix de l'évènement & développement

La documentation de composer propose l'évènement post-package-install et post-package-update, qui aurait, évidemment, parfaitement convenu à notre situation (exécuter un script après l'installation d'un paquet).

Cependant, l'évènement ne semble pas pouvoir être déclenché à volonté par le développeur : il semble qu'il faille manuellement supprimer puis ré-installer un paquet.

Nous avons préféré utiliser l'évènement post-update-cmd et post-install-cmd, qui peut être déclenché avec la commande composer run-scripts.

Notez que l'évènement post-package-* ne fournit pas une instance de Composer\Script\CommandEvent mais bien de Composer\Script\PackageEvent. La classe permet de récupérer immédiatement le package installé par Composer\Script\PackageEvent::getOperation()->getPackage() (ou Composer\Script\PackageEvent::getOperation()->getTargetPackage() dans le cas d'un update).