Les fixtures

Les fixtures

1 mai 2021

Les fixtures vont permettre d'insérer de fausses données en base de données pour nous aider à développer plus facilement. Elles sont très pratiques, car elles nous évitent de devoir systématiquement remplir des formulaires ou insérer à la main des données pour vérifier le fonctionnement de certaines parties du code.

Avant de commencer à générer nos fausses données, il nous faut installer le bundle sur notre projet Symfony :

composer require orm-fixtures --dev

Le drapeau --dev, permet de spécifier que nous pourrons utiliser le bundle seulement en développement et pas en production. D’un autre côté, de fausses données en production, ce n'est pas utile.

Dans votre dossier "src", un nouveau dossier au nom de "DataFixtures" est apparu. À l'intérieur de celui-ci se trouve un fichier nommé "AppFixtures.php". Il est tout à fait possible de créer toutes ses fixtures dans ce fichier, mais par souci d'organisation et de clarté, nous allons voir comment dissocier les différentes données dans chaque table de votre base de données.

Écrire une fixture

Tout d'abord, nous allons utiliser une commande afin de générer notre fichier. Par convention, il faut ajouter "Fixtures" à la fin du nom. Le nom de ce fichier est totalement libre, mais pour bien comprendre sur quelle entité il tourne, je lui donne le même nom que celle-ci :

symfony console make:fixtures VillesFixtures

# Vous pouvez aussi utiliser une autre commande
# php bin/console make:fixtures VillesFixtures

Dorénavant, un nouveau fichier au nom de "VillesFixtures.php" se situe dans le dossier "src/DataFixtures/".

Voyons à quoi il ressemble pour le moment :

<?php

namespace App\DataFixtures;

use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class VillesFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        // $product = new Product();// $manager->persist($product);

        $manager->flush();
    }
}

Deux commentaires nous montrent ici très rapidement comment se servir de ce fichier. Il s'agit simplement d'utiliser Doctrine et l'entité souhaitée.

<?php

namespace App\DataFixtures;

use App\Entity\Villes;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class VillesFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 20; $i++) {
           $city = new Villes();
           $city->setName('ville-'. $i);
           $manager->persist($city);
        }

        $manager->flush();
    }
}

On peut remarquer que l'on a une boucle for(), contenant une instanciation de mon entité, une hydratation des données souhaitées et une persistance des données. Une fois la boucle terminée, on rajoute une exécution des requêtes en base de données.

Pour le moment, je n'ai absolument rien en base de données ; écrire du PHP ne suffit pas, il faut aussi l'exécuter ! Pour cela, il suffit de taper la commande suivante :

symfony console doctrine:fixtures:load

# Vous pouvez aussi utiliser une autre commande
# php bin/console doctrine:fixtures:load

Répondez yes à la question suivante.

Voilà, je possède 20 noms de ville dans ma base de données !
Alors ok, le nom de mes villes ne sont pas très originaux, mais pour les essais, ça suffira largement.

Note : la commande ci-dessus purge votre base de données avant d'exécuter vos fixtures ! Si jamais vous souhaitez ajouter des données à celles déjà existantes, ajoutez le drapeau --append.

symfony console doctrine:fixtures:load --append

# Vous pouvez aussi utiliser une autre commande
# php bin/console doctrine:fixtures:load --append

Relation de table

Quand on crée des fixtures, on peut se retrouver à devoir enregistrer des informations telles que des clés étrangères pour remplir les colonnes de notre table. Or, on ne peut pas se permettre d'insérer des ID d'informations n'existant pas.

Je vais donc créer une seconde fixture pour insérer des agences. Celles-ci seront reliées à des villes.

<?php

namespace App\DataFixtures;

use App\Entity\Agences;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class AgencesFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 20; $i++) {
            $agence = new Agences();
            $agence->setName('agence-'. $i);
            $agence->setAddress('address-'. $i);
            $agence->setCreatedAt(new \DateTime());
            $agence->setVille();
            $manager->persist($agence);
        }

        $manager->flush();
    }
}

Ma fixture est presque prête à être utilisée. J'ai décidé d'insérer 20 agences. Vous remarquerez que j'ai un setter de vide : setVille(). Dans ce cas précis, je ne peux pas lui passer une chaîne de caractère ou un type numérique : Doctrine attend un objet.

Pour faire cela, l'idéal serait de récupérer un des objets de ma fixture "VillesFixtures.php", créée plus tôt, et de l'ajouter dans mon setter.

Tout est prévu ! Deux méthodes vont nous permettre de réaliser ce processus facilement :

  • addReference() : enregistre un objet
  • getReference() : récupère une référence

On va donc modifier la fixture "VillesFixtures.php" pour y ajouter un addReference() :

<?php

namespace App\DataFixtures;

use App\Entity\Villes;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class VillesFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 20; $i++) {
            $city = new Villes();
            $city->setName('ville-'. $i);
            $manager->persist($city);

            $this->addReference('ville'. $i, $city);
        }

        $manager->flush();
    }
}

À l'intérieur de ma boucle for() et après mon persist(), j'ai ajouté la ligne suivante :

$this->addReference('ville'. $i, $city);

Cette méthode attend deux paramètres. Le premier est un nom qui doit être unique ! Sinon, il écraserait l'ancienne donnée et la remplacerait par la nouvelle. J'ai donc concaténé le mot ville à ma variable "$i" de manière à être sûr d'avoir des noms différents : "ville-0, ville-1, ville-2, ...". En second paramètre, j'ajoute l'objet à enregistrer.

Il reste maintenant à récupérer ces informations dans mon autre fixture :

<?php

namespace App\DataFixtures;

use App\Entity\Agences;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class AgencesFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 20; $i++) {
            $agence = new Agences();
            $agence->setName('agence-'. $i);
            $agence->setAddress('address-'. $i);
            $agence->setCreatedAt(new \DateTime());
            $agence->setVille($this->getReference('ville'. $i));
            $manager->persist($agence);
        }

        $manager->flush();
    }
}

Dans mon setter setVille(), j'utilise la méthode getReference() en lui passant le nom de l'objet à récupérer. Étant donné que j'ai inséré 20 villes et 20 agences, j'utilise mon "$i" pour m'aider. Ainsi, à chaque itération, le nom de la référence sera : "ville-0, ville-1, ville-2, ..." Avec ce nom unique, on récupérera facilement l'objet qui y est lié.

Si vous exécutez maintenant les fixtures pour insérer des données, une belle erreur sera générée par votre terminal. Il s’avère que par défaut, les fixtures sont exécutées par ordre alphabétique.
En suivant cette logique, le mot "Agences" est placé avant "Villes". Ainsi, au moment de récupérer l'objet par le biais de la référence, cela ne fonctionnera pas ! Pourquoi ? Parce que les villes doivent être créées avant les agences, comme nous l'avions décrété. On ne peut pas récupérer des informations qui n'existent pas.

On va modifier la fixture "AgencesFixtures.php" et lui dire qu'elle dépend de la fixture "VillesFixtures.php". De cette manière, l'ordre sera modifié automatiquement pour que les villes soient créées avant les agences.

<?php

namespace App\DataFixtures;

// ...
use Doctrine\Common\DataFixtures\DependentFixtureInterface;

class AgencesFixtures extends Fixture implements DependentFixtureInterface
{
   // ...

   public function getDependencies()
   {
      return [
         VillesFixtures::class  
      ];
   }
}

Implémentez maintenant la classe "DependentFixtureInterface" et ajoutez la méthode getDependencies().

Enfin, retournez dans la méthode un tableau contenant le nom de la classe dont elle dépend. Pour moi, ce sera VillesFixtures::class.

Si vous relancez votre commande dans votre terminal, vous verrez que l'ordre a été modifié pour s'adapter à nos changements.

Faker

Jusque-là, nous avons vu comment créer et insérer quelques données grâce aux fixtures. Le souci est que les données que nous entrons de cette façon risquent de se ressembler fortement. Le mieux serait d'avoir véritablement des données aléatoires.

C'est là que le bundle Faker entre en jeu. Cette bibliothèque permet de générer de fausses données, mais bien plus convaincantes que les nôtres.

Tout d'abord, nous devons installer ce package :

composer require fzaninotto/faker

Je vais modifier ma fixture "AgencesFixtures.php" afin d'y insérer de vraies fausses données :

<?php

namespace App\DataFixtures;

use App\Entity\Agences;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Faker;

class AgencesFixtures extends Fixture implements DependentFixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $faker = Faker\Factory::create('fr_FR');

        for ($i = 0; $i < 20; $i++) {
           $agence = new Agences();
           $agence->setName($faker->company);
           $agence->setAddress($faker->address);
           $agence->setCreatedAt($faker->dateTimeBetween('-2years', 'now'));
           $agence->setVille($this->getReference('ville'. $i));
           $manager->persist($agence);
        }

        $manager->flush();
    }

    public function getDependencies()
    {
       return [
          VillesFixtures::class
       ];
    }
}

Il faut commencer par insérer le namespace de Faker :

use Faker;

Ensuite, instanciez-le dans la méthode load() et placez-le avant votre boucle for() (pas besoin de l'instance à chaque tour de boucle). En paramètre, j'ai précisé que les données seront en français ; si vous ne mettez rien, elles seront en anglais (plusieurs autres langues existent) :

$faker = Faker\Factory::create('fr_FR');

Enfin, dans chaque setter, on utilise l'objet "$faker" et on va chercher le style d'informations que l'on souhaite utiliser :

$agence->setName($faker->company);

Vous voilà maintenant avec une base de données remplie de vraies fausses données, ce qui est plus sympa tout de même.

Vous pouvez retrouver un listing complet des différentes informations disponibles sur la documentation officielle : https://github.com/fzaninotto/Faker