Gestion de la persistence (ORM) avec Doctrine

Table of Contents

1 Introduction

L'objectif de cette séance sera de se familiariser avec le module Doctrine, tel qu'utilisé par Symfony.

Le travail à réaliser consiste à mettre en place le modèle de données (le M de MVC) et tester la création de la base de données et son initialisation avec SQLite.

Partir de la description du back-office de l'application d'Agence de voyage.

Se référer à la documentation dans https://symfony.com/doc/current/book/doctrine.html (ou celle de Doctrine directement).

On testera que l'implémentation est correcte via le chargement de données de tests fournies.

2 Configurer l'utilisation de SQLite en local

Dans app/config/config_dev.yml, surcharger la définition de l'utilisation de MySQL (en prod), pour la remplacer par l'utilisation de SQLite :

# app/config/config_dev.yml
# ...
doctrine:
    dbal:
        driver: pdo_sqlite
        path: '%kernel.root_dir%/sqlite.db'
        charset: UTF8

Créer la base de données (noter l'emplacement du fichier sqlite.db créé) :

php bin/console doctrine:database:create

3 Créer les premières classes d'un modèle fonctionnel

L'objectif est de créer dans src/AppBundle/Entity/ différents fichiers définissant un premier modèle de l'application :

  • Circuit.php
  • CircuitProgramme.php
  • Etape.php
  • Client.php
  • Reservation.php
  • Avis.php

On testera que le code est valide en essayant de charger des données de test fournies.

3.1 Première classe d'entité : Circuit

On va générer le code PHP des classes d'entités pour leurs attributs de base, avec l'outil en ligne de commande doctrine:generate:entity.

3.1.1 Spécification d'un "Circuit"

On va créer l'entité "Circuit" pour répondre au modèle suivant :

Circuit( description, paysDepart, villeDepart, villeArrivee, dureeCircuit) :

  • il a une description (chaine de caractères présentant le circuit).
  • il débute dans un pays (paysDepart) à partir d'une ville (villeDepart) pour arriver dans une autre ville, éventuellement la même (villeArrivee).
  • il dure plusieurs jours (dureeCircuit);

3.1.2 Génération du code pour Doctrine

php bin/console doctrine:generate:entity --no-interaction \
    --entity="AppBundle:Circuit" \
    --fields="description:text paysDepart:string(length=30 nullable=false) villeDepart:string(length=30 nullable=true) villeArrivee:string(length=30 nullable=true) dureeCircuit:smallint(nullable=true)"

3.1.3 Vérifier le code généré

Consulter le fichier Circuit.php

3.1.4 Tester ensuite la création du schéma

php bin/console doctrine:schema:update --force

Tester que la base est bien créée avec le bon schéma :

sqlite3 app/sqlite.db
sqlite> .fullschema

3.1.5 Tester les fixtures de Circuit

Les datafixtures sont des données de test à charger dans la base de données pour la durée du développement

On se réfèrera à https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html en cas de besoin.

  1. Ajout d'un bundle nécessaire

    On utilise composer, qui gère l'installation des dépendances d'un projet (à télécharger au même endroit que l'installeur Symfony, cf. cours intro).

    php ../bin/composer.phar require --dev doctrine/doctrine-fixtures-bundle
    

    Modification de app/AppKernel.php pour déclarer l'utilisation du bundle (attention à l'emplacement : cf. https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html#step-2-enable-the-bundle ).

    ...
    class AppKernel extends Kernel
    {
        public function registerBundles()
        {
            $bundles = [
                new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
                ...
            ];
    
            if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
                $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
                ...
                $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
            }
    
       ...
    

    Ajouter le fichier src/AppBundle/DataFixtures/ORM/LoadCircuitData.php en téléchargeant le source fourni (cf. http://www-inf.it-sudparis.eu/COURS/CSC4501/TPs/TP_Symfony/agvoy/).

    Exécuter le chargement des données de tests

    php bin/console doctrine:fixtures:load
    

    Vérifier que les données sont présentes dans la table :

    sqlite3 app/sqlite.db
    
    sqlite> select * from circuit;
    1|Andalousie|Espagne|Grenade|Séville|
    2|Vietnam|VietNam|Hanoi|Hô Chi Minh|
    3|Ile de France|France|Paris|Paris|
    4|Italie|Italie|Milan|Rome|
    

    Vérifier les logs de l'application, dans var/logs/ pour vérifier les requêtes effectuées en base de données.

3.2 Ajouter l'entité "circuit programmé" (CircuitProgramme)

3.2.1 Génération du code pour les attributs simples

Utiliser à nouveau php bin/console doctrine:generate:entity pour créer CircuitProgramme.php, et y générer les attributs "simples" de l'entité CircuitProgramme. Initialement, on crée l'entité sans la gestion de la relation de programmation avec Circuit, qui sera écrite manuellement par la suite.

CircuitProgramme( circuitID, dateDepart, nombrePersonnes, prix) :

  • il est programmé avec une date de départ (dateDepart)
  • il est prévu pour un nombre de personnes maximal (nombrePersonnes)
  • il a un prix donné (prix)

3.2.2 TODO Créer l'association entre Circuit et CircuitProgramme

On ajoute une relation avec Circuit qui définit le circuit qui est programmé (en base de données, circuitID clé étrangère sur Circuit).

En s'inspirant des exemples de https://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations, modifier :

  • Circuit.php pour ajouter un attribut gérant association avec les entités CircuitProgramme.

    Circuit::programmes -> liste de CircuitProgramme(s)
    
    ...
    /**
     * @ORM\OneToMany(targetEntity="CircuitProgramme", mappedBy="circuit")
     */
    protected $programmes;
    ...
    
  • CircuitProgramme.php pour ajouter la relation inverse (pointant sur le Circuit):

    CircuitProgramme::circuit -> id de Circuit
    
    ...
    /**
     * @ORM\ManyToOne(targetEntity="Circuit", inversedBy="programmes")
     * @ORM\JoinColumn(name="circuit_id", referencedColumnName="id")
     */
    protected $circuit;
    ...
    

Générer les constructeurs et les getters et setters correspondant aux nouveaux attributs des associations :

php bin/console doctrine:generate:entities AppBundle/Entity/Circuit

ce qui ajoute les méthodes suivantes :

  • Circuit::__construct()
  • Circuit::getProgrammes()
  • Circuit::addProgramme()
  • Circuit::removeProgramme()

Puis :

php bin/console doctrine:generate:entities AppBundle/Entity/CircuitProgramme

qui ajoute :

  • CircuitProgramme::setCircuit()
  • CircuitProgramme::getCircuit()

Vérifier le code généré, et mettre à jour le schéma de la base de données :

php bin/console doctrine:schema:update --force

3.2.3 TODO Ajouter la fixture de CircuitProgramme

Ajouter src/AppBundle/DataFixtures/ORM/LoadCircuitProgrammeData.php fourni depuis http://www-inf.it-sudparis.eu/COURS/CSC4501/TPs/TP_Symfony/agvoy/

Exécuter le chargement des données de tests

php bin/console doctrine:fixtures:load

Vérifier que les données sont présentes dans la table

sqlite> select * from circuit_programme;
...

3.3 Ajouter l'entité étape (Etape)

3.3.1 Générer les attributs de base

Comme pour CircuitProgramme, on commence par les attributs simples, qu'on génère avec php bin/console doctrine:generate:entity, en laissant de côté la gestion de l'association avec Circuit pour une deuxième étape.

Etape( circuitID, numeroEtape, villeEtape, nombreJours) :

  • le numéro 1 identifie la première étape du circuit
  • l'étape arrive à une ville (villeEtape)
  • elle a une durée en jours (nombreJours)

Remarque : la durée du circuit doit correspondre à la somme des durées des étapes qui le constituent (cette contrainte sera implémentée ultérieurement)

3.3.2 Modifier le modèle pour gérer l'assocation avec Circuit

Ensuite, comme pour l'association entre Circuit et CircuitProgramme, on va modifier le modèle généré pour ajouter l'association entre un Circuit et ses Etape(s) dans Circuit.php et Etape.php.

L'association sera mise en œuvre principalement grâce à Circuit::addEtape().

Puis :

  • on regénère les getters et setters dans Etape.php;
  • on met à jour manuellement le constructeur de Circuit (qui ne peut être regénéré automatiquement car il existe déjà);
  • on met à jour le schéma de la base;
  • on teste le chargement des données avec la fixture LoadEtapeData.php téléchargée.

3.3.3 Vérifier que le modèle Symphony est fonctionnel en mémoire

On souhaite vérifier que le modèle Symphony est fonctionnel, au moyen de tests effectuées avec phpunit.

Récupérer le script de test unitaire tests/AppBundle/Entity/EtapeTest.php (depuis http://www-inf.it-sudparis.eu/COURS/CSC4501/TPs/TP_Symfony/agvoy/tests/AppBundle/Entity/EtapeTest_php.txt), et tester qu'il fonctionne avec :

phpunit tests/AppBundle/Entity/EtapeTest.php --filter EtapeTest::testCircuitOneToManyEtapes

Vérifier si cela fonctionne, et sinon, expliquer pourquoi ça ne marche pas comme on le souhaiterait (le circuit connaît ses étapes, puisque la première assertion est vérifiée, mais l'étape, elle, ne semble pas connaître son circuit… qu'est-ce qui ne va pas ?)

Lors du chargement des données de ficture, aucune erreur n'a été signalée, donc on peut essayer de retrouver les étapes d'un circuit par une requête SQL :

SELECT description, numeroEtape, villeEtape, nombreJours
       FROM circuit, etape
       WHERE etape.circuit_id = circuit.id;

Pour vous mettre sur la voie, vous pouvez vérifier le code dans LoadEtapeData.php qui charge les données des étapes :

...
class LoadEtapeData extends AbstractFixture implements OrderedFixtureInterface
{
        public function load(ObjectManager $manager)
        {
                $circuit=$this->getReference('andalousie-circuit');
        ...

        $etape = new Etape();
                $etape->setNumeroEtape(1);
                $etape->setVilleEtape("Grenade");
                $etape->setNombreJours(1);

                $circuit->addEtape($etape);

                $manager->persist($etape);
        ...             
                $manager->flush();
        }
...

Note : on peut voir une trace des requêtes SQL effectuées par Doctrine dans le fichiers de logs, dans var/logs/. Vous pouvez essayer de recharger les fixtures d'Etape, et de voir ce qui se passe dans les logs.

3.3.4 Corriger le code initialisant l'association entre Circuit et Etape

Corriger Circuit::addEtape() et rejouer les tests unitaires.

3.3.5 Vérifier que l'ajout d'une étape fonctionne bien au niveau des données sauvées en base

Une fois le problème corrigé dans Circuit::addEtape() pour le modèle en mémoire, on va vérifier qu'il ne manque pas autre chose, et notamment que la persistence des entités du modèle en base de données fonctionne bien avec Doctrine.

Pour vérifier que la persistence fonctionne bien pour l'association Circuit --- Etape, on va recharger les données de fixtures, et revérifier que les données sont réellement correctes en base.

On peut vérifier cela avec un test d'intégration phpunit, qui va vérifier que la persistence fonctionne. Vérifier que le test EtapeTest::testAssociationCircuitEtapes fonctionne bien :

Modifier app/config/config_test.yml pour ajouter une configuration alternative pour l'emplacement de la base de données SQLite, afin que le passage des tests unitaires ne perturbe pas les essais manuels effectués dans l'environnement de développement.

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   pdo_sqlite
                path:     '%kernel.cache_dir%/test.db'

Puis créer et peupler la base de tests, qu'utilise le module de tests phpunit. Attention, on travaille cette fois sur la base de tests (--env=test), et non la base de développement comme précédamment.

php bin/console doctrine:database:create --env=test
php bin/console doctrine:schema:update --env=test --force
php bin/console doctrine:fixtures:load --env=test

Vérifier que le test passe bien (attention : il suppose que les données de fixtures ont été bien chargées en base):

phpunit tests/AppBundle/Entity/EtapeTest.php --filter EtapeTest::testAssociationCircuitEtapes

Note : le test effectue une requête SQL pour effectuer une vérification de bout en bout, sans faire confiance au cache de Doctrine, qui pourrait masquer des problèmes. On peut vérifier les requêtes SQL dans les logs (var/logs/test.log) pour voir ce qui est réellement effectué, et qu'on peut avoir confiance dans les résultats du tes.

3.3.6 Vérifier que le code de suppression d'une étape d'un circuit est correct

On va automatiser le chargement des données de fixtures avant le passage de chaque cas de test, pour éviter des effets de bords, et avoir à recharger les fixtures avant chaque lancement des tests.

On s'appuie pour cela sur le bundle LiipFunctionalTestBundle.

composer require --dev liip/functional-test-bundle

Modification de app/AppKernel.php :

<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        // ...
        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            $bundles[] = new Liip\FunctionalTestBundle\LiipFunctionalTestBundle();
        }

        return $bundles
    }

    // ...
}

Modification de app/config/config_test.yml :

liip_functional_test: ~

Tester avec tests/AppBundle/Entity/AssocCircuitEtapeTest.php :

phpunit tests/AppBundle/Entity/AssocCircuitEtapeTest.php

4 Ajouter la logique métier pour la gestion des étapes d'un circuit

Notre application comporte un certain nombre de règles de gestion qui permettent d'implémenter la logique métier :

  • les étapes sont numérotées dans un certain ordre, ce qui suppose une renumérotation en cas d'ajout ou suppression d'étapes dans un cricuit
  • la durée du circuit doit correspondre à la somme des durées des étapes qui le constituent

Effectuer des modifications du code afin d'implémenter cette logique :

  1. ajouter le calcul durée dans Circuit:addEtape() et Circuit:removeEtape()
  1. tester avec le test unitaire correspondant : CircuitTest::testDureeCircuit dans tests/AppBundle/Entity/CircuitTest.php.

    Récupérer tests/AppBundle/Entity/CircuitTest.php en le téléchargeant.

    Passer le test :

    phpunit tests/AppBundle/Entity/CircuitTest.php --filter CircuitTest::testDureeCircuit
    
  2. Implémenter la renumérotation des étapes en cas de suppression d'une étape au milieu d'un circuit
  1. compléter les tests unitaires dans tests/AppBundle/Entity/CircuitTest.php pour mieux gérer différents cas

Author: Olivier Berger

Created: 2016-02-16 mar. 18:12

Validate