Gestion de la persistence (ORM) avec Doctrine
Table of Contents
- 1. Introduction
- 2. Configurer l'utilisation de SQLite en local
- 3. Créer les premières classes d'un modèle fonctionnel
- 3.1. Première classe d'entité :
Circuit
- 3.2. Ajouter l'entité "circuit programmé" (
CircuitProgramme
) - 3.3. Ajouter l'entité étape (
Etape
)- 3.3.1. Générer les attributs de base
- 3.3.2. Modifier le modèle pour gérer l'assocation avec
Circuit
- 3.3.3. Vérifier que le modèle Symphony est fonctionnel en mémoire
- 3.3.4. Corriger le code initialisant l'association entre
Circuit
etEtape
- 3.3.5. Vérifier que l'ajout d'une étape fonctionne bien au niveau des données sauvées en base
- 3.3.6. Vérifier que le code de suppression d'une étape d'un circuit est correct
- 3.1. Première classe d'entité :
- 4. Ajouter la logique métier pour la gestion des étapes d'un circuit
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.
- 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ésCircuitProgramme
.Circuit::programmes -> liste de CircuitProgramme(s)
... /** * @ORM\OneToMany(targetEntity="CircuitProgramme", mappedBy="circuit") */ protected $programmes; ...
CircuitProgramme.php
pour ajouter la relation inverse (pointant sur leCircuit
):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 :
- ajouter le calcul durée dans
Circuit:addEtape()
etCircuit:removeEtape()
tester avec le test unitaire correspondant :
CircuitTest::testDureeCircuit
danstests/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
- Implémenter la renumérotation des étapes en cas de suppression d'une étape au milieu d'un circuit
- compléter les tests unitaires dans
tests/AppBundle/Entity/CircuitTest.php
pour mieux gérer différents cas