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 :

{% highlight php %}

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)
{}

}

{% endhighlight %}

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

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é :

{% highlight php %}

// 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);

        # ...

    }

}

}

{% endhighlight %}

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 :

{% highlight json %}

“autoload”: { “psr-4”: {“Chill\Composer\” : “app/Composer/”} }

{% endhighlight %}

  • instructions pour exécuter les scripts après chaque commande composer update et composer install :

{% highlight json %} “scripts”: { “post-install-cmd”: [ “Chill\Composer\Migrations::synchronizeMigrations” ], “post-update-cmd”: [ “Chill\Composer\Migrations::synchronizeMigrations” ] }

{% endhighlight %}

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).