Archives de catégorie : ORM

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (3/3)

Voici la dernière partie de mon billet concernant les migrations Doctrine et le bundle associé.

Les relations des entités Doctrine

Nos élèves ont une relation avec leur filière. Nous en avons parlé au tout début lorsque nous définissions les cardinalités :

  • un élève a une seule filière
  • une filière a 0 ou plusieurs élèves

doctrine
Il va nous falloir définir ceci au niveau de nos entités Doctrine. Nous allons donc « tirer un fil » entre nos entités Eleve et Filiere. Doctrine a des moyens de mettre ça en place et nous allons voir comment ceci se matérialise avec les annotations, que nous avons choisi de privilégier lorsque nous avons généré nos entités.

Entre Eleve et Filiere, il y a une relation de un à plusieurs (one to many en anglais). Nous la voulons bi-directionnelle dans le sens où nous souhaitons pouvoir à partir d’un élève récupérer sa filière et, à partir d’une filière, récupérer ses élèves. Dans l’entité Eleve, nous sommes du côté plusieurs de la relation (car plusieurs élèves correspondent à une filière). Voici comment nous allons nous servir des annotations Doctrine :

   /**
     * @ORM\ManyToOne(targetEntity="Filiere", inversedBy="eleves")
     * @ORM\JoinColumn(name="filiere_id", referencedColumnName="id", nullable=false)
     **/
    private $filiere;

Nous définissons une variable d’instance privée filiere qui va aider Doctrine à stocker la filière d’un élève (il n’y en a qu’une possible, souvenez-vous). La cible de la relation que nous sommes en train de décrire dans l’entité Eleve est l’entité Filiere, c’est ce que nous dit targetEntity. Le inversedBy qui suit signifie que de l’autre côté de la relation (dans l’entité Filiere) nous trouverons l’attribut chargé de gérer cette relation sous le nom d’eleves (notez bien le pluriel car de l’autre côté de la relation nous aurons plusieurs élèves pour notre filière). JoinColumn décrit la manière physique dont cette relation va exister. En lisant, vous devinez que dans notre table nous allons avoir un champ rajouté suite à l’écriture de cette relation qui va identifier une filière pour un élève, que ce champ s’appellera filiere_id et qu’il référencera une colonne appelée id…Vous voyez déjà venir gros comme une maison la contrainte d’intégrité référentielle. Le nullable=false signifie qu’il faut obligatoirement une filière à un élève ; là aussi vous devez imaginer que cette clause entraînera la présence d’un NOT NULL dans le schéma relationnel.

Du côté de l’entité Filiere, nous avons l’annotation Doctrine suivante pour la variable d’instance eleves dont nous avons déjà parlé :

     /**
     * @ORM\OneToMany(targetEntity="Eleve", mappedBy="filiere")
     **/
    private $eleves;

    public function __construct()
    {
        $this->eleves = new ArrayCollection;
    }

Nous prenons la relation dans l’autre sens – « un vers plusieurs » – parce qu’une filière a plusieurs élèves. Voilà pourquoi cette fois c’est one to many en lieu et place du many to one qu’on trouvait dans Eleve. La cible, c’est bien Eleve et l’attribut qui va servir c’est bien filiere (private $filiere). Dans Eleve nous avions inversedBy, dans Filiere nous avons mappedBy. Doctrine détermine le côté « possesseur » de la relation par inversedBy et l’autre côté (« inverseur ») par mappedBy.

Comme nous avons potentiellement plusieurs élèves pour une filière, nous allons avoir à gérer plusieurs objets. Pour ce faire, Doctrine nous propose son type ArrayCollection, que vous allez pouvoir utiliser en faisant :

use Doctrine\Common\Collections\ArrayCollection;

Notre variable d’instance privée eleves sera de ce type et nous l’initialiserons dans le constructeur de notre classe.

Nous avons construit notre première relation au niveau objet ! L’heure est venue de mettre notre schéma de bases de données à jour avec tout cela ! Allons-y !

app/console doctrine:migrations:diff

Ouvrons le fichier ainsi généré pour vérifier que notre relation est bien en place :

$this->addSql("ALTER TABLE eleve ADD filiere_id INT NOT NULL");
$this->addSql("ALTER TABLE eleve ADD CONSTRAINT FK_ECA105F7180AA129 FOREIGN KEY (filiere_id) REFERENCES filiere (id)");
$this->addSql("CREATE INDEX IDX_ECA105F7180AA129 ON eleve (filiere_id)");

Ce que nous envisagions est effectivement arrivé, nous avons bel et bien une contrainte d’intégrité référentielle posée entre la table eleve et la table filiere !

Lançons maintenant la migration :

app/console doctrine:migrations:migrate

Notre table eleve a bien un champ en plus : filiere_id !

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; desc eleve'
Enter password: 
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| nom        | varchar(40) | NO   |     | NULL    |                |
| prenom     | varchar(40) | NO   |     | NULL    |                |
| age        | int(11)     | NO   |     | NULL    |                |
| filiere_id | int(11)     | NO   | MUL | NULL    |                |
+------------+-------------+------+-----+---------+----------------+

Occupons-nous ensuite des cours: un cours a des relations avec salle, enseignant et filière, en fait cours est au centre de tout…Quelles sont-elles exactement ? Reprenons ce que nous avions établi :

  • une filière est composée d’un ou plusieurs cours
  • un cours n’a qu’un seul et unique enseignant mais un enseignant a un ou plusieurs cours
  • un cours se tient dans une seule salle à un moment donné et une salle peut abriter plusieurs cours. On peut très bien créer un cours sans salle car il arrive qu’on ne puisse décider de suite dans quelle salle il se tiendra. Elle sera rajoutée plus tard.

Les relations nous apparaissent évidentes. Un lien de type « un à plusieurs » existe entre cours et enseignant (« un à plusieurs » dans le sens enseignant → cours et « plusieurs à un » dans le sens inverse). Il en va de même pour les salles ou les filières. La seule différence réside en le fait qu’un cours a forcément un enseignant ou une filière alors qu’il n’a pas forcément de salle (on peut le créer sans salle pour lui en affecter une quand on a consulté le planning des salles et qu’on en a trouvé une de disponible). Nous allons donc nous retrouver à ajouter des annotations de type ManyToOne dans notre entité Cours.

    /**
     * @ORM\ManyToOne(targetEntity="Filiere", inversedBy="cours")
     * @ORM\JoinColumn(name="filiere_id", referencedColumnName="id", nullable=false)
     **/
    private $filiere;

    /**
     * @ORM\ManyToOne(targetEntity="Salle", inversedBy="cours")
     * @ORM\JoinColumn(name="salle_id", referencedColumnName="id")
     **/
    private $salle;

    /**
     * @ORM\ManyToOne(targetEntity="Enseignant", inversedBy="cours")
     * @ORM\JoinColumn(name="enseignant_id", referencedColumnName="id", nullable=false)
     **/
    private $enseignant;

Vous noterez que salle est le seul endroit où nullable est à sa valeur par défaut, c’est à dire true et qu’il sera donc possible d’enregistrer un cours sans salle. Dans la mesure où nous avons pour un cours soit 0 (pour une salle) soit un (enseignant, filière) inutile ici d’utiliser les ArrayCollection comme c’était le cas pour matérialiser la relation filière/élève.
Dans chacune de ces entités cibles nous auront une variable d’instance nommée cours, c’est aussi ce que nous disent ces annotations.

Dans l’entité Salle :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="salle")
     **/
    private $cours;

    public function __construct()
    {
        $this->cours = new ArrayCollection;
    }

Ici par contre une salle est susceptible d’accueillir plusieurs cours (NFA057 le jeudi soir, NFA053 le mardi), la notion de collection a ici du sens. Idem pour les enseignants :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="enseignant")
     **/
    private $cours;

    public function __construct()
    {
        $this->cours = new ArrayCollection;
    }

Ou encore Filiere :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="filiere")
     **/
    private $cours;

Vous avez remarqué que le champ mappedBy de l’annotation pointe vers la variable d’instance correspondante dans l’entité Cours. Dans le cas de Filiere, l’entité avait déjà une instanciation de la classe ArrayCollection dans son constructeur, nous devons en rajouter une autre pour les cours, comme fait dans les autres entités, plus haut, ce qui donnera :

public function __construct()
    {
        $this->cours = new ArrayCollection;
        $this->eleves = new ArrayCollection;
    }

Une fois que nous avons mis nos relations dans nos 4 entités, il nous reste à exécuter une migration !

app/console doctrine:migrations:diff
app/console doctrine:migrations:migrate

feront apparaître les contraintes d’intégrité référentielle entre la table cours et enseignant, salle et filiere.

Maintenant que nous avons établi notre architecture objet avec nos entités, regardons avec MySQL Workbench à quoi ressemble notre schéma et surtout s’il satisfait à toutes les contraintes que nous avons énoncées au début du présent document :

snapshot2

En naviguant à travers les relations, nous sommes sûrs que nous avons effectué du bon travail car elles correspondent en tous points à ce que nous souhaitions.

Au sujet des types de données utilisés par Doctrine, nous voyons qu’ils ne correspondent pas forcément à ce que nous recherchons en terme de plage de valeurs (des INTEGER pour gérer des âges – pas signés qui plus est -, des DATETIME alors que des TIMESTAMP, deux fois moins gourmands en espace disque, suffiraient). Il y a beaucoup à redire de ce côté là sur ce schéma mais sachez qu’il est toujours possible de forcer Doctrine à utiliser des types choisis par nos soins avant de passer une migration ! Il suffit d’en faire état dans les annotations de nos entités. Attention cependant, changer le type des données choisies par Doctrine peut faire que les changements d’état de votre schéma (de la colonne modifiée, pour être plus exact) ne sont pas pris en compte comme ils devraient l’être par Doctrine, qui ne retrouve plus ses types « natifs », puisque vous les avez modifiés.

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (2/3)

Maintenant que nous avons installé notre bundle dédié aux migrations des données via Doctrine, nous pouvons créer le bundle qui va servir de support à notre exercice et que nous allons appeler Scolarite car il est censé matérialiser le fonctionnement de notre université, la fabrique de développeurs ! Voici le résumé de la création de notre bundle en mode interactif :

Symfony/app/console generate:bundle
Bundle namespace: FabriqueDeDevs/ScolariteBundle
Bundle name [FabriqueDeDevsScolariteBundle]: 
Target directory [/home/sebastien.ferrandez/migrations/Symfony/src]: 
Configuration format (yml, xml, php, or annotation): yml
Do you want to generate the whole directory structure [no]? yes
You are going to generate a "FabriqueDeDevs\ScolariteBundle\FabriqueDeDevsScolariteBundle" bundle
in "/home/sebastien.ferrandez/migrations/Symfony/src/" using the "yml" format.

Generating the bundle code: OK
Checking that the bundle is autoloaded: OK
Confirm automatic update of your Kernel [yes]? 
Enabling the bundle inside the Kernel: OK
Confirm automatic update of the Routing [yes]? 
Importing the bundle routing resource: OK

doctrine-logo

Les cours à la fabrique de développeurs

Nous souhaitons modéliser de manière très simpliste la gestion des cours; des élèves s’inscrivent chez nous qui choisissent des filières composées d’un certain nombre de cours. Ces cours sont assurés par des enseignants dans des salles. Nous établissons les règles suivantes, de manière tout à fait arbitraire :

  • un élève appartient à une seule filière
  • une filière est composée d’un ou plusieurs cours
  • un cours n’a qu’une seule filière
  • un enseignant a un ou plusieurs cours
  • un cours n’a qu’un seul et unique enseignant mais un enseignant a un ou plusieurs cours
  • un cours se tient dans une seule salle à un moment donné et une salle peut abriter plusieurs cours. On peut très bien créer un cours sans salle car il arrive qu’on ne puisse décider de suite dans quelle salle il se tiendra, celle-ci sera rajoutée plus tard.

J’ai mis en gras les objets du domaine d’application qui vont donc être éligibles au titre d’entité. Nous allons commencer par les plus simples et nous allons tout générer en ligne de commande sans passer par le mode interactif :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Eleve \

--fields="nom:string(40) prenom:string(40) age:integer"

Ce que nous allons faire systématiquement c’est changer l’annotation Doctrine laissée vierge et qui contient le nom que la table cible va avoir.
Rendons nous dans le fichier nouvellement généré :

vi Symfony/src/FabriqueDeDevs/ScolariteBundle/Entity/Eleve.php

Et rajoutons la ligne en gras dans son entête:

/**
 * Eleve
 *
 * @ORM\Table(name="eleve")
 * @ORM\Entity
 */

Maintenant que nous avons généré une première entité, notre schéma de bases de données va devoir refléter ce changement, c’est à dire passer de l’état initial 0 à l’état 1 « une table élève est à créer ».
Nous faisons usage de l’argument diff :

Symfony/app/console doctrine:migrations:diff

Un fichier est généré :

Generated new migration class to "/home/sebastien.ferrandez/migrations/Symfony/app/DoctrineMigrations/Version20130420192803.php" from schema differences.

Vous devinez aisément le format de ces fichiers : VersionYYYYMMDDHHMMSS. Voyons ce que celui-ci contient :

class Version20130420192803 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB");
    }

    public function down(Schema $schema)
    {
        // this down() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("DROP TABLE eleve");
    }
}

Vous voyez d’emblée deux méthodes, up et down; la première est exécutée dans le sens ascendant, c’est à dire en cas de migration et l’autre dans le sens descendant, dans le cas d’une rétro-migration (d’un rollback). On retrouve dans l’un les actions antagonistes de l’autre, si dans up vous avez un create table, vous aurez un drop table dans le down.
C’est dans ce fichier que vous pouvez changer les types de données dans vos requêtes SQL avant génération des éléments de la base de données ! Quoiqu’il en soit, les changements du diff ne sont pas appliqués à la base de données tant que vous n’avez pas utilisé la commande migrate :

Symfony/app/console doctrine:migrations:migrate

Voici ce que me donne à l’écran l’exécution de cette commande :

Migrating up to 20130420192803 from 0

  ++ migrating 20130420192803

     -> CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.08s)

  ------------------------

  ++ finished in 0.08
  ++ 1 migrations executed
  ++ 1 sql queries

Je migre bien de l’état 0 vers l’état 20130420192803 en faisant un CREATE TABLE eleve. Tout se passe conformément à ce que nous imaginions…Continuons avec notre deuxième entité, celle qui concerne nos enseignants :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Enseignant \

--fields="nom:string(40) prenom:string(40) age:integer"

Nous allons faire la même chose que précédemment :

  • donner, dans notre entité, un nom à la table qui va être créée
  • faire un diff
  • faire un migrate

Le diff produit un fichier de migration qui va cette fois concerner la table enseignant, dont nous venons de mettre le nom dans l’entité qui va la gérer. Le migrate, lui, va donner :

WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20130420214445 from 20130420192803

  ++ migrating 20130420214445

     -> CREATE TABLE enseignant (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.05s)

  ------------------------

  ++ finished in 0.05
  ++ 1 migrations executed
  ++ 1 sql queries

Nous en profitons pour voir comment évolue les tuples de table migration_versions, gérée par Doctrine :

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; select * from migration_versions'
Enter password: 
+----------------+
| version        |
+----------------+
| 20130420192803 |
| 20130420214445 |
+----------------+

Vous voyez que toute commande migrate entraîne la création d’une ligne dans cette table ! Nous continuons avec la création des autres entités :

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Salle \
--fields="nom:string(40)"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Cours \
--fields="nom:string(40) date:datetime duree:integer"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Filiere\
--fields="nom:string(40)"

L’idée est d’isoler chaque changement du schéma de base de données (pour faire plus simple, chaque création de table) dans une migration. Si nous passons une gigantesque migration qui contient TOUT, en cas de nécessité de rétro-migrer, nous nous retrouverions à faire un grand pas en arrière au lieu de reculer à petits pas.
Nous avons fini avec les entités, nous allons à présent dans la dernière partie, « raccorder » celles qui doivent l’être ! A suivre…

symfony_black_03

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (1/3)

Nous allons voir comment se servir du bundle de migrations fourni par Doctrine avec Symfony et Composer. Placez vous dans le répertoire qui servira de base à cet exercice. Pour ma part j’ai choisi la localisation suivante :

mkdir $HOME/migrations && cd $HOME/migrations

Nous allons tout d’abord installer Composer dans ce répertoire. Bien évidemment, il vous faudra disposer de curl. Si vous ne l’avez pas installé, faites-le de suite :

sudo apt-get install curl

Ensuite installez Composer :

curl -sS https://getcomposer.org/installer | php
#!/usr/bin/env php
All settings correct for using Composer
Downloading...

Composer successfully installed to: /home/sebastien.ferrandez/migrations/composer.phar
Use it: php composer.phar

Maintenant que nous avons Composer installé, nous allons faire une installation de Symfony via cet outil :

php composer.phar create-project symfony/framework-standard-edition Symfony/ 2.2.1

Des bundles sont déployés sur votre machine (citons Doctrine, Swiftmailer, Monolog, Assetic) et vous les trouverez en faisant :

ls -l Symfony/vendor/

Vous vous en doutez, tous ne nous seront pas utiles. Pour nettoyer votre install et garder ce que vous voulez, n’hésitez pas à mettre à jour votre fichier composer.json.
Prenons quelques secondes pour regarder ce composer.json généré par notre create-project :

cat Symfony/composer.json

Attardons nous sur la partie « require » de notre objet JSON :

    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.2.*",
        "doctrine/orm": "~2.2,>=2.2.3",
        "doctrine/doctrine-bundle": "1.2.*",
        "twig/extensions": "1.0.*",
        "symfony/assetic-bundle": "2.1.*",
        "symfony/swiftmailer-bundle": "2.2.*",
        "symfony/monolog-bundle": "2.2.*",
        "sensio/distribution-bundle": "2.2.*",
        "sensio/framework-extra-bundle": "2.2.*",
        "sensio/generator-bundle": "2.2.*",
        "jms/security-extra-bundle": "1.4.*",
        "jms/di-extra-bundle": "1.3.*"
    }

Vous y retrouvez tous les bundles dont vous avez vu défiler le nom à l’installation, c’est plutôt rassurant !

L’heure est maintenant venue d’installer le bundle de migrations Doctrine. Vous êtes toujours à la racine de votre projet et vous exécutez :

php composer.phar require doctrine/doctrine-migrations-bundle dev-master -d Symfony
composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing doctrine/migrations (v1.0-ALPHA1)
    Downloading: 100%         

  - Installing doctrine/doctrine-migrations-bundle (dev-master 99c0192)
    Cloning 99c0192804134a8c1d0588777bd98bdbc2dbb554

Vous refaites un :

cat Symfony/composer.json

Pour vérifier que votre fichier composer.json a été mis à jour. Normalement, la partie require devrait maintenant ressembler à ça :

    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.2.*",
        "doctrine/orm": "~2.2,>=2.2.3",
        "doctrine/doctrine-bundle": "1.2.*",
        "twig/extensions": "1.0.*",
        "symfony/assetic-bundle": "2.1.*",
        "symfony/swiftmailer-bundle": "2.2.*",
        "symfony/monolog-bundle": "2.2.*",
        "sensio/distribution-bundle": "2.2.*",
        "sensio/framework-extra-bundle": "2.2.*",
        "sensio/generator-bundle": "2.2.*",
        "jms/security-extra-bundle": "1.4.*",
        "jms/di-extra-bundle": "1.3.*",
        "doctrine/doctrine-migrations-bundle": "dev-master"
    },

Si c’est le cas, alors vous avez installé le bundle avec succès ! Il nous faut cependant faire quelques « réglages »; commençons par ajouter notre bundle fraîchement installé dans le fichier AppKernel.php

vi Symfony/app/AppKernel.php

Nous y rajoutons la ligne suivante (vous pouvez laisser la dernière virgule car il s’agit d’une dangling comma) :

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),

Ensuite il nous faut évidemment renseigner nos paramètres d’accès à la base de données afin que Doctrine puisse s’y connecter et donc faire :

vi Symfony/app/config/parameters.yml

Vous renseignerez dans ce fichier les paramètres de votre base de données (le driver utilisé – dans le doute laissez PDO -, l’IP de la machine cible, votre nom d’utilisateur et enfin le mot de passe).Dans la partie database, mettez le nom de votre base de données: assurez-vous bien entendu qu’elle existe ! La mienne s’appelle « exercice », je la crée sans plus attendre en ligne de commande (mon serveur MySQL est sur ma machine locale, je ne précise donc pas -h localhost) :

mysql -u root -p -e 'create database exercice'

Pour m’assurer que tout est en place avant de m’amuser avec les migrations, je lance une dernière commande :

sebastien.ferrandez@sebastien:~/migrations$ Symfony/app/console doctrine:migrations:status

 == Configuration

    >> Name:                        Application Migrations
    >> Database Driver:             pdo_mysql
    >> Database Name:               exercice
    >> Configuration Source:        manually configured
    >> Version Table Name:          migration_versions
    >> Migrations Namespace:        Application\Migrations
    >> Migrations Directory:        /home/sebastien.ferrandez/migrations/Symfony/app/DoctrineMigrations
    >> Current Version:             0
    >> Latest Version:              0
    >> Executed Migrations:         0
    >> Executed Unavailable Migrations: 0
    >> Available Migrations:            0
    >> New Migrations:                  0

Tout semble fonctionner ! Comme je suis d’un naturel curieux et que je ne crois que ce que je vois, je regarde si des choses se sont passées dans ma base de données :

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; show tables'
Enter password: 
+--------------------+
| Tables_in_exercice |
+--------------------+
| migration_versions |
+--------------------+

Et la réponse est OUI ! Une nouvelle table a été créée pour gérer les versions de mes migrations ! Tout est maintenant réuni pour s’adonner sans retenue aux joies de la migration avec Doctrine !
Rendez-vous donc bientôt dans la partie 2 pour y voir du concret !!!