TP n°2 - Manipuler les données avec Doctrine
Aller plus loin que les requêtes basiques
Table des matières
- 1. Objectifs de cette séquence
- 2. Étape 1 : Observation des requêtes sur l’application mini-allociné
- 2.1. TODO Étape 1.a : Mise en place de la séance
- 2.2. TODO Étape 1.b : Génération de la base de données de développement dans SQLite
- 2.3. Mais où sont nos données ?
- 2.4. TODO Étape 1.c : Génération de données de tests
- 2.5. TODO Étape 1.d : Observation des Requêtes SQL de chargement
- 2.6. TODO Étape 1.e : Modification de l’ordre de tri des films par défaut
- 2.7. TODO Étape 1.f : Consultation de la documentation Symfony au sujet de Doctrine
- 2.8. TODO Naviguer dans la référence : l’API de Doctrine
- 3. Étape 2 : Nouvelles commandes pour l’ajout de films, et recommendations
- 4. Auto-évaluation
- 5. Pour la suite
- 6. Aller plus loin (optionnel)
- 7. Annexe
1 Objectifs de cette séquence
Cette séquence de travail a pour objectif de compléter la découverte des fonctionnalités de Doctrine pour la manipulation des données en PHP
La séance de travail précédente a permis de se familiariser avec l’environnement de travail du développeur Symfony, et avec le composant d’accès aux données Doctrine.
L’application « mini-allociné » en ligne de commande comportait uniquement la consultation de la base de données. On va maintenant lui ajouter des fonctions permettant de modifier des données.
À l’issue de cette séance, on disposera d’une application en ligne de commande permettant d’assurer la persistence en base de données en PHP objet avec Doctrine, ce qui permettra de développer les fondements d’une application Web Symfony typique.
2 Étape 1 : Observation des requêtes sur l’application mini-allociné
Cette étape vise à manipuler les outils liés à Doctrine, pour mieux maîtriser la mise au point d’une application Symfony au niveau de la couche d’accès aux données.
2.1 TODO Étape 1.a : Mise en place de la séance
Vous allez dupliquer l’arborescence du projet PHP obtenu à la fin de la séance précédente, pour travailler sur une nouvelle version.
Vous pourrez ainsi faire évoluer le code, et revenir en arrière si besoin, en comparant l’état à la fin de la séance prédente, avec l’état courant.
Effectuez les opérations suivantes pour dupliquer le dossier :
cd $HOME/CSC4101 cp -r tp-01 tp-02 cd tp-02 cd miniallocine
Ensuite, dans le projet, on va réinitialiser les rouages du projet avec Composer :
cd $HOME/CSC4101/tp-02/miniallocine rm -fr composer.lock symfony.lock var/cache/ vendor/ symfony composer install
Cette étape est nécessaire car Symfony s’appuie sur une mécanique
sophistiquée de mise en cache (dans var/cache/
) du code de
l’application, pour assurer des performances
maximales. Malheureusement, si on duplique un projet, une partie de ce
cache a tendance à devenir incohérente (présence de chemins d’accès « en dur », etc.).
Il est donc préférable de réinitialiserle projet radicalement : à
un état le plus « neuf » possible. On fait ensuite réinstaller par Composer
les bibliothèques du quadriciel Symfony (installées dans vendor/
), et
reconfigurer certains fichiers de configuration associés.
La documentation Symfony préconise d’utiliser symfony console
cache:clear
pour ce genre d’opérations, mais nous adoptons ici une
approche plus radicale, qui évite certains problèmes étranges dans des
environnements de travail exotiques, ou cache:clear
ne semble pas
suffisant.
Ces manipulations pourront se révéler nécessaires dès que vous aurez à
réorganiser votre environnement de travail, migrer le code d’une
machine à l’autre, ou d’un compte à l’autre. Gardez un marque-page
quelque part pour pouvoir vous y référer.
Chargez maintenant dans votre IDE cette nouvelle version du projet Symfony « mini-allociné » sur laquelle vous allez travailler.
Dans Eclipse vous pouvez importer le nouveau projet dans le même workspace que le précédent, et fermer le projet précédent (menu Project / Close …).
Certains préféreront peut-être l’approche gestion de versions de l’application dans Git, plutôt que la duplication dans différents répertoires. Cette approche est totalement valide, mais pour ne pas alourdir le contenu de la séance avec la manipulation de Git, nous avons préféré une approche plus basique (certains diron archaïque).
2.2 TODO Étape 1.b : Génération de la base de données de développement dans SQLite
L’objectif de cette étape est d’utiliser les outils de génération de la base de données intégrés dans Doctrine.
Durant le développement de nos applications, le modèle de données évoluera progressivement. Vous serez amenés à répéter régulièrement les opérations que nous décrivons ici. Souvenez-vous-en, ou notez-les dans un fichier de notes qui vous suivra pendant les différentes séances de TP et/ou le projet.
On va re-créer la base de données de l’application miniallocine
qui nous
sert à effectuer des tests
pendant le développement et la mise au point du code. On travaillera toujours en ligne de
commande depuis l’intérieur du projet Symfony de l’application miniallocine
.
Effectuez les opérations suivantes :
Supprimez le fichier de base de données SQLite existant :
cd miniallocine/ rm -f var/data.db
Créez un fichier de base de données SQLite (vide), en exécutant la commande suivante :
symfony console doctrine:database:create ls -l var/data.db
En fait, il se trouve que cette commande se contente de créer un fichier vide, ce qui correspond au comportement nécessaire pour une base de données légère comme SQLite. Mais dans le cas où on utiliserait un autre SGBD, ceci peut s’avérer nécessaire.
- Créez le schéma de la base SQLite (tables, index, etc.), en utilisant
la sous-commande
doctrine:schema:update
:Commencez-donc par vérifier ce qui serait fait :
symfony console doctrine:schema:update --dump-sql
The following SQL statements will be executed: CREATE TABLE recommendation (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, film_id INTEGER NOT NULL, recommendation VARCHAR(255) NOT NULL); CREATE INDEX IDX_433224D2567F5183 ON recommendation (film_id); CREATE TABLE film (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, year INTEGER NOT NULL);
Comparez avec les éléments dans la classe PHP présente dans le module
src/Entity/
de notre application. Est-ce que cela correspond au modèle des données souhaité (défini dans les annotations@ORM
de nos classes PHP) ?Le code SQL affiché devrait vous sembler assez clair, même si certains identifiants (clés étrangères, index) sont générés de façon arbitraire. Pour le reste : classes, attributs, ça correspond normalement aux noms de vos classes ou méthodes PHP.
Essayez de créer le schéma avec :
symfony console doctrine:schema:update
Symfony est très prudent, car il ne le fait pas : trop dangereux… vous devez confirmer explicitement cette opération potentiellement destructrice.
- Forcez explicitement la création du schéma pour de bon.
Si vous consultez le contenu de la base présent dans var/data.db
, avec
sqlite3
(ligne de commande) ou sqlitebrowser
(interface graphique), vous pourrez afficher le schéma de la base :
$ sqlite3 var/data.db SQLite version 3.27.2 2019-02-25 16:06:06 Enter ".help" for usage hints. sqlite> .schema CREATE TABLE recommendation (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, film_id INTEGER NOT NULL, recommendation VARCHAR(255) NOT NULL); CREATE TABLE sqlite_sequence(name,seq); CREATE INDEX IDX_433224D2567F5183 ON recommendation (film_id); CREATE TABLE film (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, year INTEGER NOT NULL); sqlite> .exit
Au fur et à mesure des développements, le schéma de la base de données
peut être mis à jour (ajout d’un attribut, ajout d’une classe…). À chaque fois que l’on fait évoluer le modèle de
données dans les classes PHP de src/Entity/
, il faut alors exécuter
à nouveau la commande doctrine:schema:update
.
Doctrine permet donc de générer complètement la base de données, de façon assez transparente pour le programmeur PHP (notamment sans avoir à se soucier des spécificités de chaque SGBD : MariaDB vs PostgreSQL vs …). Le développeur se concentre sur son modèle objet, sur le code PHP et ses annotations/attributs Doctrine, sans avoir besoin d’apprendre SQL (la chance ?).
Dans notre application « pour jouer », on illustre un cas où on part de
zéro, et où les développeurs ont conçu un modèle de données objet
d’abord, dans le code PHP objet, et où on en dérive la base de données
nécessaire à la persistence de ces données.
Dans la « vraie vie », on peut avoir le cas inverse, où la base de
données existe déjà, et où on doit développer une application Symfony
qui s’y connecte. Dans ce cas, on peut utiliser Doctrine aussi, mais
il ne s’agira pas de créer le schéma de la base avec les outils qu’on
vient de voir.
2.3 Mais où sont nos données ?
Le schéma de notre base de données a été créé par Doctrine à partir des directives de notre code PHP objet.
Nous disposons maintenant d’une base de données pour tester
l’application. L’application fonctionne (symfony console
app:list-films
), même si comme la base est encore vide, il ne se
passe pas grand chose d’intéressant.
Voyons maintenant comment ajouter des données de tests dans la base SQLite.
2.4 TODO Étape 1.c : Génération de données de tests
L’objectif de cette étape est d’expérimenter le module de Data Fixtures qui permet de charger des données dans la base de données nouvellement créée (vide), pour tester l’application.
On pourrait injecter des données dans la base via un script SQL, mais cela nécessiterait d’avoir préalablement bien compris comment gérer les différentes clés étrangères, par exemple. Il y aurait des chances que les données injectées soient difficiles à générer « à la main » en écrivant le code SQL directement (erreurs d’identifiants, violations de contraintes, etc.)
Contrairement à cette approche, le module Data Fixtures permet d’écrire ce chargement des données en PHP, en utilisant les méthodes des classes du modèle de données basé sur Doctrine.
Cela nous assure qu’on exécute bien le code PHP du modèle de notre application. Cela permet de tester par là-même que le code de l’application fonctionne bien.
Les manipulations d’identifiants sont évitées, en utilisant à la place des alias mnémotechniques, ce qui rend bien plus simple la mise au point des jeux de données, et diminue les bugs : une seule référence : le code PHP. Ici on n’aura pas à avoir de doutes entre qui a raison : le code PHP ou le code SQL.
Vous allez utiliser ce module de Symfony couplé à Doctrine, gérant des Data Fixtures :
Chargez les données de tests :
symfony console doctrine:fixture:load
Cette commande vous indique qu’elle charge les données à partir du module PHP dont le namespace est «
App\DataFixtures\AppFixtures
».Vérifiez le contenu de la base de données (
sqlite3
ousqlitebrowser
). Par exemple, la commande suivante affiche un « dump » du contenu de la base :sqlite3 var/data.db .dump
Vous avez normalement récupéré les mêmes données que dans l’état initial de la base, que vous aviez utilisée pendant le TP précédent.
- Consultez le code des fixtures dans les classes PHP présentes dans
src/DataFixtures/AppFixtures.php
. On vous avait fait ajouter ce code par copier/coller dans la séance précédente.
Quelle version vous semble-t-elle plus lisible / compréhensible : le dump SQL ou le code objet PHP s’appuyant sur les Fixtures de Doctrine ?
Le code de la classe AppFixtures
introduit quelques nouveautés PHP
comme l’instruction yield
permettant de construire des générateurs
(cf. Generator
syntax), pour construire une collection d’attributs, un peu comme on
mettrait nos données de texte dans une feuille de tableur.
Ce code est une bonne pratique dans le développement d’applications Symfony. Nous ne l’avons pas inventé : il est recommandé dans la documentation de Symfony et Doctrine.
Observez le code de load( )
: il utilise les données brutes
pour instancier en mémoire des objets PHP de notre modèle de données
(instances de App\Entity\Film
), appeler leurs setters pour
initialiser les valeurs des attributs, et enfin, demander à Doctrine
de sauvegarder ces objets ($manager->persist()
). On voit bien
comment ce code teste effectivement le modèle des données, tout en
remplissant la base. Si les setters sont incorrects, cela sera détecté.
Ce module Data Fixtures est intéressant dans une démarche de tests
continus au cours du développement (/Test Driven Development)
Il permet de construire un jeu de données de tests,
au fur et à mesure du développement, pour permettre
des tests automatiques de la partie Modèle de l’application.
En recréant périodiquement la base de données, et en rechargeant les
données de test, on peut assurer un minimum de tests de non-régression.
2.5 TODO Étape 1.d : Observation des Requêtes SQL de chargement
Sous le capot, Doctrine utilise des requêtes SQL SELECT pour le chargement des données.
Dans la configuration en ligne de commande de notre application actuelle, on n’a pas tous les outils Symfony activés directement pour avoir des informations de mise au point (debug). Mais on va essayer d’ajouter une trace (log)des requêtes effectuées.
Ajouter le composant
symfony-doctrine-sql-logger-bundle
au projet pour obtenir des logs des opérations Doctrine DBAL :symfony composer require halloverden/symfony-doctrine-sql-logger-bundle
Exécutez une des commandes de l’application :
symfony console app:list-films
Regardez les traces apparaissant dans le fichier var/log/dev.log
au
fur et à mesure des commandes (ouvrez par exemple un autre terminal et
exécutez-y : tail -f $HOME/CSC4101/tp-02/miniallocine/var/log/dev.log
)
Vous verrez apparaître les requêtes SELECT générées, par exemple,
pour un findAll()
:
2019-07-22T16:54:46+02:00 [debug] SELECT t0.id AS id_1, t0.title AS title_2, t0.year AS year_3 FROM film t0
On verra plus tard, quand on sera en mode Web, qu’on aura à notre disposition des traces de ces requêtes dans la barre d’outils de mise au point de Symfony.
Ce genre d’éléments de diagnostic nous permet d’essayer de traverser les couches des différents composants du cadriciel, pour comprendre ce qui se passe, sans avoir à modifier le code.
Tous les frameworks possèdent des fonctionnalités de debug, avec entre autre la possibilité d’avoir des messages plus verbeux, des traces, etc. (en mode développement, au moins; cf. Troubleshooting Problems).
En cas de bugs, ou de doute, il sera souvent nécessaire (en plus de
lire les messages d’erreur de Symfony), d’aller consulter les logs
dans var/log/dev.log
. Au départ, certains messages peuvent sembler
obscurs, mais apprendre à les décoder peut sauver des vies (presque,
mais au moins de l’argent, pour commencer !).
Gardez en mémoire cette façon de faire, elle va vous servir souvent.
2.6 TODO Étape 1.e : Modification de l’ordre de tri des films par défaut
On va maintenant essayer de modifier l’ordre de tri par défaut des films, lors du chargement des données, pour avoir les films triés par année.
Pour mémoire, nous avons présenté certaines fonctionnalités de Doctrine dans les transparents du cours CM 1-2 (Partie 5 : Couche d’accès aux données avec l’ORM Doctrine, également disponible dans Moodle).
Il suffit de modifier pour cela la classe FilmRepository
, qui a été
générée dans src/Repository/FilmRepository.php
, pour surcharger la
méthode findAll()
qu’elle hérite d’une classe parente.
Vous pouvez par exemple coder ceci, pour surcharger la méthode par défaut findAll()
en utilisant une méthode plus flexible (findBy()
) en lui spécifiant les arguments de tri :
/** * {@inheritDoc} * @return Film[] * @see \Doctrine\ORM\EntityRepository::findAll() */ public function findAll(): array { // findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) return $this->findBy( array(), array('year' => 'ASC', 'title' => 'ASC') ); }
Testez que cela change bien la requête effectuée symfony console
app:list-films
.
Mais comment savoir comment on peut écrire ce bout de code, sans passer des semaines à lire la doc (ou bien comprendre quelle est la meilleure façon de faire à la lecture des dizaines de messages sur des forums répondant plus ou moins à cette question…) ?
Commençons par examiner la documentation
2.7 TODO Étape 1.f : Consultation de la documentation Symfony au sujet de Doctrine
Savoir trouver et lire la documentation de référence est une
compétence importante pour les ingénieurs.
Ça a l’air trivial (et un truc de boomer) d’apprendre à lire la doc… pourtant le temps passé à lire la documentation est souvent profitable par rapport au temps passé à faire le tri dans les réponses de StackExchange.
Notez que Symfony, Doctrine, et toutes les
bibliothèques du framework de développement qu’on utilise évoluent
en permanence. Il est donc important de savoir sur quelle version du
framework on
travaille (nous, c’est la LTS, la 5.4.x actuellement).
En cas de doute : symfony console about
.
Pour lire les documentations pertinentes, attention à ne pas regarder
celles trop vieilles (Symfony v3), mais pas trop récentes (Symfony
v6)…
Nous avons fait de notre mieux pour indiquer des liens vers les « bonnes versions », dans notre contexte :
Symfony 5.4.x, Doctrine 2.13, etc.
Méfiance en cas de recherches dans un moteur de recherche, dans les
forums, etc.
Ouvrez la documentation relative à la gestion des données avec Doctrine dans Symfony : Databases and the Doctrine ORM
Vous pouvez parcourir rapidement cette documentation.
Les explications sont utiles, mais font référence au contexte d’une application Web, or nous n’avons pas encore expérimenté avec le contexte Web. Le code des classes Controller ne vous est donc pas familier.
Mais vous devriez néanmoins repérer du code similaire à celui de notre application « mini-allociné », par exemple dans Fetching objects from the Database :
$repository = ...->getRepository(Product::class); // look for a single Product by its primary key (usually "id") $product = $repository->find($id); // look for a single Product by name $product = $repository->findOneBy(['name' => 'Keyboard']); // or find by name and price $product = $repository->findOneBy([ 'name' => 'Keyboard', 'price' => 1999, ]); // look for multiple Product objects matching the name, ordered by price $products = $repository->findBy( ['name' => 'Keyboard'], ['price' => 'ASC'] ); // look for *all* Product objects $products = $repository->findAll();
Espérons que cela vous paraît compréhensible.
Doctrine met en œuvre un patron de conception assez courant dans les
couches d’abstraction sur l’accès aux données, le Repository
, qu’on
retrouve dans beaucoup d’autres cadriciels et langages.
Si on veut creuser un plus loin, il faut lire la doc du projet Doctrine lui-même.
Doctrine est un projet libre à part entière pour fournir des utilitaires d’accès aux données en PHP, qui évolue indépendamment de Symfony. Sa documentation donne donc des exemples en PHP, mais pas forcément toujours adaptés au contexte Symfony
Pour connaître toutes les variantes possibles pour faire des requêtes, cette documentation donne plus de détails sur le fonctionnement des repository d’entités : Documentation Doctrine ORM / Working with Objects / By Simple Conditions
Que faire de toute cette documentation ? Le but est de savoir où elle est, et de s’y référer, autant que de besoin, et surtout sur la version qui nous concerne. C’est parfois un jeu de piste, et parfois la documentation manque…
Examinons alors comment se référer à la documentation ultime : le code des composants qu’on utilise.
2.8 TODO Naviguer dans la référence : l’API de Doctrine
Revenons à la modification du code de
src/Repository/FilmRepository.php
qu’on vous a proposée pour
modifier le tri effectué au de chargement des données.
Ici on développe en objet, et le principe des classes Repository de
notre modèle de données dans src/Repository/
est de surcharger celui
par défaut de Doctrine.
Pour modifier le comportement, il faut d’abord penser objet : surcharger le comportement par défaut en spécialisant le comportement de notre repository, par rapport au repository générique de la classe de base.
Mais encore faut-il trouver la documentation de l’API (Application Programming Interface) de Doctrine qui codifie ce comportement par défaut…
En fait, en l’occurrence, ici, les développeurs de Doctrine ne publient malheureusement pas sur le Web une documentation spécifique de l’API.
Mais dans ce cas, on doit se référer au code. Et ça tombe bien, car ce code contient des commentaires (docstrings) que l’IDE peut exploiter pour nos guider dans l’utilisation des API des bibliothèques comme Doctrine.
Le code de la classe Doctrine EntityRepository
donne plus de détails
sur l’API Doctrine
(cf. code sur GitHub de lib/Doctrine/ORM/EntityRepository.php
):
Tout d’abord, par défaut, findAll()
est juste un appel à findBy()
avec les options par défaut :
public function findAll() { return $this->findBy([]); }
Ensuite, pour findBy()
, sa doctring précise :
/** * Finds entities by a set of criteria. * * @param int|null $limit * @param int|null $offset * @psalm-param array<string, mixed> $criteria * @psalm-param array<string, string>|null $orderBy * * @return object[] The objects. * @psalm-return list<T> */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) [...]
Vous comprenez donc mieux le rôle des arguments qu’on passe à findBy()
dans FilmRepository::findAll()
:
array()
: premier tableau attendu,$criteria
(le critère filtre duWHERE
de la requête SQL), ici un tableau vide, donc pas de filtre à appliquer;array('year'=>'ASC', 'title'=>'ASC')
deuxième tableau (optionnel)$orderBy
, pour les critères de tri (paryear
puis partitle
).
Savoir lire le code des projets qu’on utilise est une compétence importante des ingénieurs, notamment aujourd’hui où la plupart des projets utilisent des composants libres plus ou moins bien documentés.
Use the source, Luke !
3 Étape 2 : Nouvelles commandes pour l’ajout de films, et recommendations
Dans cette étape, on va expérimenter avec les fonctionnalités de Doctrine permettant l’ajout de données dans la base.
Vous allez ajouter de nouvelles commandes (app:add-film
,
app:add-recommendation
, …), utilisables en console, dans notre
application mini-allociné.
Vous utiliserez à nouveau le générateur de code qu’on a utilisé dans la séance précédente, pour l’ajout du squelette de code de la classe gestionnaire.
3.1 TODO Étape 2.a : Ajout de la commande d’ajout de films
Sur le modèle des ajouts de commandes effectués dans la séance
précédente, ajoutez une commande app:add-film
qui prend deux
arguments (titre, année) et va ajouter le film correspondant à la base
de données.
symfony console make:command
Voici un squelette de code à adapter, qui permet d’ajouter des données dans la base via Doctrine :
use Symfony\Component\DependencyInjection\ContainerInterface; use Doctrine\ORM\EntityManager; use App\Entity\Film; //... class AddFilmCommand //... { /** * @var EntityManager : gère les fonctions liées à la persistence */ private $em; public function __construct(ContainerInterface $container) { parent::__construct(); $this->em = $container->get('doctrine')->getManager(); } //... protected function execute(InputInterface $input, OutputInterface $output): int { //... // crée une instance en mémoire $film = new Film(); $film->setTitle($title); $film->setYear($year); // marque l'instance comme "à sauvegarder" en base $this->em->persist($film); // génère les requêtes en base $this->em->flush(); return Command::SUCCESS; } }
Examinez les commentaires du code ci-dessus, qui devraient déjà vous donner suffisamment d’informations pour arriver à faire marcher cette commande.
Une fois codé la méthode AddFilmCommand::execute()
, testez là pour voir passer les requêtes SQL générées par Doctrine dans le fichier de logs :
symfony console app:add-film "Mon film préféré" 2022
Vérifiez-bien que vous avez bien obtenu les requêtes SQL INSERT
désirées.
En cas de problèmes (commande non reconnue), n’hésitez pas à exécuter la commande suivante, pour mettre au propre l’environnement d’exécution alors que le code a évolué :
symfony console cache:clear
3.2 TODO Étape 2.b : coder des ajouts sur les associations OneToMany
Sur le modèle de la portion de code précédente, écrivez enfin une commande
app:add-recommendation
permettant d’ajouter une recommendation à un
film existant.
C’est l’étape la plus difficile de cette séance.
Dans notre modèle de données, les recommendations n’existent de façon isolée, mais dans le contexte d’un film. La commande doit donc prendre 3 arguments en entrée :
- les références du film existant : titre et année,
- et la chaine de caractères à ajouter dans la recommendation.
Vous pourrez utiliser les éléments suivants pour mettre en œuvre ce code (un peu plus compliqué que les précédents) :
// Chargement en mémoire d'un film existant dans la base $film = $filmRepository->findOneBy( ['year' => $year, 'title' => $title]); // Création d'une instance en mémoire $recommendation = new Recommendation(); $recommendation->setRecommendation($recotext); // Ajout en mémoire dans la collection des recommendations de ce film $film->addRecommendation($recommendation);
Pour le reste, ça ressemble beaucoup à la commande précédente (appel à persist()
, flush()
, etc.).
Testez de-même en ligne de commande :
symfony console app:add-recommendation "Mon film préféré" 2022 "Un beau nanard!"
Vérifiez que cela fonctionne, et que les requêtes SQL INSERT sont bien transmises à la base SQLite.
Vérifiez avec symfony console app:show-film
que vous retrouvez bien les recommendations pour leur film.
Nous reviendrons plus tard dans le cours sur les détails d’utilisation de Doctrine pour la sauvegarde des données dans la base, mais notez déjà quelques éléments clé.
Remarquez qu’ici, on fera un persist($film)
alors que le nouvel objet, dont les données sont à sauvegarder avec un INSERT
, est la
recommendation.
Cela nous permet de revenir sur la conception en base de données d’associations 1-N
/ OneToMany, vue l’année dernière.
Le comportement de l’ORM Doctrine a été défini tout d’abord en fonction de nos réponses lors de l’utilisation de l’assistant générateur de code, dans la séance précédente :
- nous avions créé la classe
Recommendations
liée àFilms
par une « relation » OneToMany (« association 1-N », en vocabulaire Entités-Associations). Danssrc/Entity/Film.php
on retrouve donc l’attribut de type CollectionFilm::recommendations
avec un attribut/annotation Doctrine#[ORM\OneToMany]
(ou@ORM\OneToMany
en PHP 7.4) - puis nous avions configuré explicitement l’option
cascade: ["persist"]
(« Étape 2-g : Ajout de la persistence en cascade »).
Cela signifie qu’on souhaite la génération d’ajouts automatique en
base (en cascade), dans la table « fille » de cette relation OneToMany
(ici Recommendation
), si un élément
a été ajouté à la collection en mémoire, au moment où on invoque
persist
sur l’entité « mère » (Film
).
Le fait qu’on ait utilisé Film::addRecommendation($recommendation)
sur $film
,
puis $this->em->persist($film)
entraine donc une insertion dans la
table qui stocke cette entité liée. La table films
n’a pas changé,
en base, c’est une nouvelle ligne dans recommendations
qui référence
films
, qu’il faut ajouter.
L’ORM Doctrine fait le job. Vous voyez que l’INSERT
transporte bien
le film_id
dans la valeur de la clé étrangère, comme prévu.
Bravo, vous avez presque tout ce qu’il vous faut pour avoir une couche de gestion des données utilisable dans une vraie application, maintenant que vous savez coder des modifications dans la base de données.
Si la mise au point de AddRecommendationCommand::execute()
est trop difficile, consultez l’annexe en fin de page qui vous propose un exemple trivial.
3.3 Gestion des erreurs
Dans les codes proposés ci-dessus, vous aurez peut-être remarqué qu’on n’a pas traité les cas d’erreur, et seulement supposé que tout se passe bien, pour simplifier les exemples.
Ainsi, dans l’ajout d’une recommendation avec app:add-recommendation
,
on suppose que le film pour lequel on ajoute la recommendation a bien
été trouvé par l’appel findOneBy()
. Bien évidemment, si l’utilisateur
saisit des informations ne correspondant à aucun film, ce code va
sûrement moins bien marcher…
Nous verrons plus tard comment gérer les erreurs au mieux, mais vous
pouvez éventuellement réléchir au sujet, et examinant le rôle du code
de retour de la méthode execute()
.
La gestion des cas d’erreur sera abordée plus tard dans le contexte Web, avec la notion d’exceptions.
4 Auto-évaluation
À l’issue de cette séquence de travail autonome, vous savez :
- expliquer le rôle de l’ORM Doctrine, pour utiliser l’approche Objet à partir d’un modèle de données programmé dans des classes PHP
- identifier la documentation de Doctrine pour pouvoir vous y référer
- identifier les fonctions de chargement des données via les méthodes des repositories dans le code PHP
- manipuler des données de tests pour permettre la mise au point et les tests de l’application avec les fixtures
- observer les requêtes SQL dans les traces pour aider à la mise au point
- recréer la base de données et son schéma
- sauvegarder des données nouvelles, issues des instances construites en mémoire
- coder l’ajout de données dans la base depuis le modèle objet avec Doctrine
- comprendre le mécanisme de persistence « en cascade » dans les collections pour les associations OneToMany
5 Pour la suite
5.1 Nettoyage de la version du TP 1
6 Aller plus loin (optionnel)
6.1 Ajout de la suppression
Vous pouvez essayer d’ajouter des commandes de suppression sur le modèle de ce qu’on a fait pour les ajouts, en vous référant à la documentation Deleting an Object.
Rien de très fou à en attendre, à part que ça marche : DELETE
powa ;-)
La modification marcherait de même… pour générer des UPDATE
dans la base.
Ne vous inquiétez pas, nous aurons l’occasion de revenir sur les mécanismes de CRUD (Create, Request, Update, Delete), dans le contexte d’une application Symfony sur le Web, dans les prochaines séances.
6.2 Lecture du tutoriel Doctrine
Vous pouvez éventuellement lire le début du guide de démarrage de Doctrine : Getting Started with Doctrine
Essayez de tester le code correspondant si vous le souhaitez, mais sans dépasser, au total, pour cette semaine le de temps de travail prévu pour ce cours.
Vous pouvez éventuellement télécharger le code associé depuis l’archive suivante : https://github.com/doctrine/doctrine2-orm-tutorial/archive/master.zip ou en clonant le repository Git du projet GitHub correspondant.
7 Annexe
Proposition d’implémentation de AddRecommendationCommand::execute()
pour l’ajout des entités liées dans une relation OneToMany :
protected function execute(InputInterface $input, OutputInterface $output): int { $title = $input->getArgument('title'); $year = $input->getArgument('year'); $recotext = $input->getArgument('recommendation'); // chargement du film de cette recommendation $filmRepository=$this->em->getRepository(Film::class); $film = $filmRepository->findOneBy( ['year' => $year, 'title' => $title]); // crée une instance en mémoire $recommendation = new Recommendation(); $recommendation->setRecommendation($recotext); // ajout en mémoire dans la collection des recommendations de ce film $film->addRecommendation($recommendation); // marque l'instance de film comme "à sauvegarder" en base $this->em->persist($film); // génère les requêtes en base $this->em->flush(); return Command::SUCCESS; }