Écriture d’une application avec interface ligne de commande « mini-allociné »
Application PHP en ligne de commande « Mini-Allociné »
Table des matières
- 1. Introduction
- 2. Étape 1 : Création d’un nouveau projet Symfony
- 3. Étape 2 : Mise en place du modèle de données du « Mini-Allociné »
- 3.1. Description du modèle des données souhaité
- 3.2. Rappel sur le rôle d’un ORM
- 3.3. TODO Étape 2-a : Configuration du projet dans le fichier
.env
- 3.4. TODO Étape 2-b : Ajout de l’entité principale : « Film »
- 3.5. TODO Étape 2-c : Ajout d’une entité secondaire : Recommendation
- 3.6. TODO Étape 2-d : Observation du code PHP objet généré
- 3.7. TODO Étape 2-e : Création de la base de données SQLite
- 3.8. TODO Étape 2-f : Ajout du chargement de données de test
- 3.9. TODO Étape 2-g : Ajout de la persistence en cascade
- 3.10. TODO Étape 2-h : Vérification du contenu de la base de données
- 4. Étape 3 : Ajout de l’interface console
- 5. Étape 4 : Chargement de données liées dans une association OneToMany
- 6. Étape 5 : Gestion de la base de données pour les tests pendant le développement
- 6.1. TODO Étape 5-a : Génération de la base de données SQLite
- 6.2. TODO Étape 5-b : Avantages de la gestion des données de tests via les Fixtures
- 6.3. TODO Étape 5-c : Observation des Requêtes SQL de chargement
- 6.4. TODO Étape 5-d : Modification de l’ordre de tri des films par défaut
- 6.5. TODO Étape 5-e : Consultation de la documentation Symfony au sujet de Doctrine
- 7. Étape 6 : Ajout de fonctions de modification des données
- 8. Ajout d’un contrôleur Web ?
- 9. Évaluation
- Aller plus loin : Amélioration des fonctionnalités (optionnel)
- DONE Annexe
1. Introduction
Cette séquence de travail permet de renforcer la connaissance de l’environnement de programmation PHP et de mise au point basé sur Symfony, et d’appréhender le fonctionnement de la couche d’accès aux données du composant Doctrine.
On va créer une application codée en PHP avec Symfony, appelée « mini-allociné », qui va accéder à une base de données relationnelle, et afficher le résultat de requêtes sur la sortie standard.
Cette application sera utilisable en ligne de commande, et illustre principalement le fonctionnement de la couche d’accès aux données, codée en PHP objet, dans une application Symfony.
Cette mini-application doit permettre de gérer des recommendations de films. On reprend ici le principe de la petite application « Mini-Allociné » étudiée à TSP dans le module CSC 3101 (Algorithmique et langage de programmation), et en réaliser une version en PHP. Pour mémoire, il s’agissait dans la séance de CSC3101 de « concevoir un petit serveur Web fournissant des informations cinématographiques. […] nous ne nous occupons que de deux fonctionnalités : celle permettant d’accéder à la liste des films gérés par le site et celle permettant de lire les avis que des utilisateurs ont déposé sur un film ».
1.1. Méthodologie
Les étudiants vont enchaîner une vingtaine d’opérations permettant de se familiariser avec les outils du quadriciel Symfony et les fonctionnalités du composant Doctrine.
Même si l’essentiel du travail peut être réalisé en copier/coller (pour simplifier dans cette première séance), il sera utile de rester attentif au rôle des différentes manipulations, pour être capable de les reproduire ultérieurement.
N’hésitez pas à prendre des notes dans un fichier que vous conservez d’une séance à l’autre.
1.2. Environnement de travail BYOD
Vous devez disposer des outils nécessaires sur votre machine BYOD, ou
travailler sur une machine DISI distante.
Vous avez dû préalablement installer et configurer ces outils, grâce
au support de la séquence de travail hors-présentiel qui précède cette
séance (cf. wiki dans l’espace Moodle du cours).
1.3. Structure de la séquence
Cette séquence de travail permet de travailler sur 4 grands domaines :
- Création d’un projet Symfony
- Mise en place d’un modèle de données objet via des classes PHP basées sur Doctrine
- Chargement des données de la base
- Gestion de la persistence des objets dans la base de données
Les composants liés au Web seront abordés par la suite. Pour l’instant en reste encore en local en ligne de commande.
1.4. Prérequis
On considère que vous avez acquis les notions de base sur la syntaxe du langage PHP, dans la séquence de Travail en Autonomie qui précède cette séance.
Du point de vue théorique, cette séance revisite principalement les acquis des cours d’informatique de l’année passée, sur le modèle objet et les bases de données relationnelles.
1.5. Format de ce document
Le présent support de séance de TP est consultable via une mise en forme avec l’habillage org-html-themes, qui permet de masquer/révéler les différentes sections du support.
Des raccourcis clavier (indiqués dans un cartouche en bas à droite)
permettent de naviguer directement dans les différentes tâches (TODO
) à
réaliser, pour aider à se concentrer sur l’avancement pas-à-pas.
On verra plus tard dans le cours comment les technologies autour de CSS et Javascript permettent de faire fonctionner ce genre de changements d’aspect dans un document HTML.
2. Étape 1 : Création d’un nouveau projet Symfony
L’objectif de cette séquence est de créer un projet Symfony minimal, prêt à accueillir nos développements.
2.1. TODO Étape 1-a : Création d’un projet Symfony
L’objectif de cette séquence est d’installer dans un sous-répertoire tous les éléments de base d’un nouveau projet Symfony.
Vous allez utiliser la commande new
de l’outil Symfony en ligne de
commande (Symfony CLI), qui va créer le répertoire de développement
d’une application Symfony et y télécharger le code source d’un
squelette d’application.
Sous le capot, cette commande s’appuie sur le gestionnaire de paquetages PHP Composer.
Exécutez les commandes suivantes :
mkdir $HOME/CSC4101/tp-mini-allocine/ cd $HOME/CSC4101/tp-mini-allocine/ symfony --debug new --no-git miniallocine --version=lts
Cet appel à symfony new
affiche des infos intéressantes pour
comprendre ce qui se passe (on pourrait se dispenser de l’option
--debug
pour afficher moins de détails) :
Creating a new Symfony 6.4 project with Composer ... [OK] Your project is now ready in /.../tp-mini-allocine/miniallocine
Composer a téléchargé dans un nouveau sous-répertoire miniallocine/
le squelette d’un projet d’application
Symfony, pour une version précise de Symfony, ici la 6.4. Comme on
a exécuté la commande symfony new --version=lts
, nous avons
spécifié de nous baser précisément sur la
version LTS (Long-Term Support) de Symfony, autrement dit une
version maintenue dans le temps, plutôt que de nous baser sur la toute
dernière version stable du framework.
Ici, avec la commande symfony new
, nous partons d’un squelette de
projet Symfony standard, sans aucune classe de modèle de
données. Seules les bibliothèques sont installées, et le coeur du
framework Symfony, mais tout le reste est à coder (ou générer).
Dans les séquences de TP sur l’application « fil-rouge », c’est
différent car on y utilise symfony composer create-project
qui part
d’un squelette préparé par l’équipe enseignante et qui contient
déjà du code applicatif.
Le but de la présente séquence est justement de voir l’ensemble des étapes nécessaires à la recréation de l’ensemble du modèle de données, par exemple.
Notez que dans ce module, on insistera souvent sur la lecture des messages affichés par les outils, notamment les messages d’erreur. Soyez attentifs aux détails.
2.2. TODO Étape 1-b : Observation du contenu du projet créé
Vérifiez le contenu du répertoire miniallocine
du projet Symfony qui vient d’être créé.
ls miniallocine/
Pour l’instant, repérez les éléments principaux qui vont nous intéresser :
bin/ composer.json composer.lock config/ public/ src/ symfony.lock var/ vendor/
composer.json
descriptif du projet Composer de l’application Symfony. Il référence notamment les bibliothèques PHP utilisées;
Vous pouvez consulter son contenu dans un éditeur de textes (format JSON). Au cours de la vie du projet Symfony on pourra utiliser Composer pour y ajouter des dépendances du projet pour effectuer le téléchargement des bibliothèques de développement additionnelles dont on aura besoin.
bin/console
- script exécutable PHP présent dans le
sous-répertoire
bin/
, utilisé en ligne de commande pour le développement Symfony derrière les appels àsymfony console
; src/
- sources du squelette d’application Symfony. Il contient le code PHP de base de l’application sur laquelle vous travaillerez par la suite;
var/
- répertoire dans lequel Symfony gère une partie de sa machinerie, avec notamment le cache de pré-compilation des bibliothèques PHP
vendor/
- contient le code de toutes les bibliothèques PHP du framework Symfony qui seront utilisées. Elles ont été téléchargées par composer.
2.3. Étape 1-c : Ajout de composants au projet Symfony
Nous aurons besoin de différents modules pour permettre à l’application de fonctionner, ou pour nous aider dans le prototypage de cette application :
- Doctrine
- le composant d’accès à la base de données via des objets PHP
- le « Maker bundle »
- qui va nous permettre de générer du code PHP plutôt que d’avoir à tout coder à la main
- les « DataFixtures »
- qui permettent de charger automatiquement des données de test dans la base de données
Procédez à l’installation avec les commandes suivantes à l’intérieur du répertoire miniallocine
:
cd miniallocine/
Gestionnaire de logs monolog
symfony composer require symfony/monolog-bundle
ORM Doctrine :
symfony composer require symfony/orm-pack
Validez l’exécution à la question posée par l’assistant (« y »)
« Maker bundle » :
symfony composer require --dev symfony/maker-bundle
DataFixtures :
symfony composer require --dev doctrine/doctrine-fixtures-bundle
Composer a téléchargé et extrait les différentes bibliothèques PHP pour Symfony. On va donc pouvoir les utiliser dans notre code.
2.4. TODO Étape 1-d : Chargement du projet dans votre IDE
Vous pouvez alors charger les sources du projet dans votre IDE PHP (que vous avez installé dans la séquence de travail hors-présentiel précédente).
Nous vous indiquons les étapes correspondantes pour l’IDE Eclipse ci-dessous. Pour d’autres IDE, la procédure est différente, mais l’essentiel est de pouvoir éditer le code PHP, et d’avoir le support de composer opérationnel dans l’IDE.
2.4.1. Rappel: Conseils pour la gestion du Workspace Eclipse
Un Workspace Eclipse (espace de travail) permet de sauvegarder, entre deux séances de travail, la configration de l’IDE, avec la liste des projets en cours, etc.
Attention à ne pas utiliser comme répertoire de stockage de ce Workspace, un répertoire interne à un projet Symfony.
On pourra par exemple conserver un Workspace unique dans
$HOME/CSC4101/workspace/
, qui référencera les différents projets PHP
qu’on importera dans l’espace de travail.
Au fur et à mesure qu’on
travaillera dans d’autres sous-répertoires de $HOME/CSC4101/
, on
obtiendra la structure de répertoire suivante :
$HOME/CSC4101/
workspace/
demo-symfony/
tp-01/
tp-mini-allocine/
tp-mini-allocine/miniallocine/
- …
- …
Les imports des différents projets Composer/Symfony présents dans les sous-répertoires les fera apparaître dans une seule hiérarchie dans l’espace de travail, vu de l’interface d’Eclipse :
- demo-symfony
- miniallocine-tp-1
- …
Cette configuration sera sauvegardée dans ce seul Workspace stocké
dans $HOME/CSC4101/workspace/
, de façon séparée de l’emplacement des
code source des projets.
On pourra garder différents projets ouverts simultanément dans Eclipse (pour faire du copier/coller par exemple), ou les fermer (dans le workspace) sans pour autant les supprimer du disque.
2.4.2. Chargement des sources de miniallocine
dans Eclipse
- Démarrez Eclipse
Dans le dialogue « Select a directory as workspace », choisissez de charger un autre Workspace que celui par défaut (Browse), et créez un nouveau répertoire dans
$HOME/CSC4101/
appeléworkspace
, par exemple.Une fois sélectionné, le chemin du champ Workspace pointe sur ce nouveau chemin
/..../CSC4101/workspace
.- Cliquez sur Launch. Eclipse se lance. Fermez la sous-fenêtre d’accueil « Welcome »
- Dans le Project explorer, sélectionnez Import projects… et dépliez le contenu de l’arbre sous « PHP ».
- Choisissez Existing Composer Project, puis Next
Cliquez sur Browse en face de Source folder, et sélectionnez le répertoire correspondant à votre
$HOME/CSC4101/tp-mini-allocine/miniallocine
dans le sélecteur de fichiers. Validez.Le dialogue détecte les paramètres du projet :
- Project name :
miniallocine
(renseignez-le s’il n’est pas détecté automatiquement) - Source folder :
/.../CSC4101/tp-mini-allocine/miniallocine
Cliquez sur Finish
- Project name :
Le projet se charge et Eclipse indexe le contenu des sources PHP (« DLTK indexing in progress »).
3. Étape 2 : Mise en place du modèle de données du « Mini-Allociné »
On va attaquer la réalisation de l’application par l’ajout de la couche d’accès aux données, de persistence, dans le modèle en 3 couches (3 tiers) qu’on a vu en cours.
3.1. Description du modèle des données souhaité
Comme dans l’application Java du Mini-Allociné de CSC3101, on souhaite gérer des Films et leurs Recommendations.
Dans notre application, on va gérer ces éléments aussi bien dans la base de données relationnelle, qui stockera les données entre deux exécutions de notre programme, qu’en mémoire durant l’exécution du programme PHP objet.
On va donc gérer les structures de données en s’appuyant :
- sur le modèle relationnel (étudié en CSC3601), pour le stockage dans la base de données.
- sur le modèle objet (étudié en CSC3101), pour la représentation en mémoire.
En base de données, on aura donc deux relations :
Film
Recommendation
Les deux seront liées par une association 1-n : « un film a plusieurs recommendations / une recommendation porte sur un film ». Une clé étrangère permettra donc d’enregistrer, dans la table des recommendations, l’identifiant du film sur lequel elle porte.
Du côté objet en mémoire, on utilisera ici la bibliothèque PHP Doctrine, qui est un ORM (Object Relational Mapper).
3.2. Rappel sur le rôle d’un ORM
Le rôle de l’ORM (Object Relational Mapper) est de permettre la gestion de collections d’objets, d’associations entre entités, et de fournir le chargement/sauvegarde des objets depuis la base de données.
On reviendra plus en détail sur Doctrine dans les séquences de cours qui suivent.
Pour faire court, le principe de l’ORM est de convertir chaque instance d’une relation (chaque ligne d’une table de la base de données) en instance d’une classe PHP en mémoire, et inversement. Doctrine appelle Entité ces classes d’objets.
Le programmeur peut ainsi écrire des classes PHP utilisant les fonctionnalités de Doctrine, et ainsi gérer des instances des entités, ou des collections d’entités. Quand deux entités sont liées (association 1-n, par exemple), des fonctions permettent de gérer cette association « OneToMany » et d’accéder à la collection des entités liées facilement.
Pour nous faciliter la tâche, nous allons utiliser un assistant de génération de code (du Maker bundler Symfony). Il va nous permettre d’ajouter dans le code source PHP une entité au modèle des données de l’application, en répondant simplement à une série de questions. Cela va nous éviter d’avoir à écrire le code PHP correspondant (mais on pourra toujours le modifier plus tard, comme si c’est nous qui l’avions écrit « à la main »).
3.3. TODO Étape 2-a : Configuration du projet dans le fichier .env
Il est maintenant nécessaire de configurer des variables de l’environnement de développement, pour pouvoir tester le fonctionnement du code de l’application avec un base de données locale SQLite.
Symfony gère la notion d’environnements ayant différentes configurations au sein d’un même projet : développement, tests, production.
Nous sommes actuellement en phase de développement, en local sur notre
machine, avec déjà pas mal d’outils pour tester notre code. Dans l’environnement de développement et de mise au point correspondant (dev
), vous utiliserez le SGBD
SQLite qui sera suffisant pour nos besoins (SQLite stocke une base de
données dans un fichier et permet de faire des requêtes SQL sur son contenu).
Modifiez le fichier (caché) .env
présent à la racine du projet, qui définit
des variables d’environnement, comme les paramètres d’accès à la base
de données.
Chargez le le fichier
.env
dans l’IDEAttention, c’est un fichier caché… il faut éventuellement le faire apparaître dans l’IDE.
Dans ce fichier
.env
, modifiez la valeur de la variableDATABASE_URL
pour prendre la valeur :DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db
a priori, il suffit de commenter la ligne présente par défaut (qui propose d’utiliser PostGreSQL), et de décommenter celle pour SQLite, un peu au-dessus.
Ainsi, la base de données SQLite pourra être créée dans le fichier local
$HOME/CSC4101/tp-mini-allocine/miniallocine/var/data.db
.
Bravo, vous êtes prêts à tester, mais avant tout à coder (ou plutôt à générer du code, dans un premier temps).
3.4. TODO Étape 2-b : Ajout de l’entité principale : « Film »
On ajoute la première entité du modèle des données, en générant le code source d’une classe PHP qui utilise Doctrine
Commençons par l’ajout de l’entité Film
ayant deux propriétés title
(titre du film) et year
(année de sortie).
Si vous êtes comme la plupart des programmeurs, vous aimez modérément
copier-coller du code qui pourrait être généré automatiquement.
Ça tombe bien, Symfony propose différent assistants qu’on ne va pas se
priver d’utiliser pour générer la base du code de nos applications,
plutôt que d’écrire nous-même du code (buggé).
On va utiliser l’assistant Symfony make:entity
, dans le terminal, depuis l’intérieur
du projet Symfony. Il va nous servir à générer le code d’une classe PHP
pour gérer notre entité « Film ».
Lancez la commande suivante, et répondez aux questions de l’assistant pour obtenir une interaction similaire à la trace présentée ci-dessous :
symfony console make:entity
Class name of the entity to create or update (e.g. TinyElephant): > Film created: src/Entity/Film.php created: src/Repository/FilmRepository.php Entity generated! Now let's add some fields! You can always add more fields later manually or by re-running this command. New property name (press <return> to stop adding fields): > title Field type (enter ? to see all types) [string]: > string Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Film.php Add another property? Enter the property name (or press <return> to stop adding fields): > year Field type (enter ? to see all types) [string]: > ? Main types * string * text * boolean * integer (or smallint, bigint) * float Relationships / Associations * relation (a wizard 🧙 will help you build the relation) * ManyToOne * OneToMany * ManyToMany * OneToOne Array/Object Types * array (or simple_array) * json * object * binary * blob Date/Time Types * datetime (or datetime_immutable) * datetimetz (or datetimetz_immutable) * date (or date_immutable) * time (or time_immutable) * dateinterval Other Types * json_array * decimal * guid Field type (enter ? to see all types) [string]: > integer Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Film.php Add another property? Enter the property name (or press <return> to stop adding fields): > Success! Next: When you're ready, create a migration with make:migration
L’assistant a créé pour nous deux classes PHP qui utilisent Doctrine :
src/Entity/Film.php
: gère les instances en mémoire des filmssrc/Repository/FilmRepository.php
: gère le chargement des films depuis la base de donnée
Rafraîchissez votre projet dans Eclipse si nécessaire pour voir apparaître ces deux classes.
Attention à lire attentivement les questions (ainsi que les messages en réponse), et à éviter le copier/coller un peu rapide.
3.5. TODO Étape 2-c : Ajout d’une entité secondaire : Recommendation
L’objectif de cette étape est de mettre en œuvre une association 1-N (ou OneToMany) dans le modèle de données, en ajoutant une entité secondaire « fille » de cette relation.
De façon similaire, on peut utiliser le générateur de code make:entity
pour ajouter une entité Recommendation
, qui ne
comporte qu’une propriété recommendation
pour le moment.
Mais cette fois, l’assistant va nous permettre de déclarer qu’on
souhaite ajouter également la gestion d’une
association entre notre entité Recommendation
et l’entité Film
existante.
Cette association sera matérialisée par la propriété film
de la classe
Recommendation
(Recommendation::film)
, de type relation
et de multiplicité
ManyToOne
. Recommendation::film
sera une référence au film sur lequel porte une
recommendation (plusieurs recommendations portent sur le même film :
« many recommendations to one film »).
Lancez à nouveau la commande suivante, et répondez aux questions de l’assistant pour obtenir une interaction similaire à la trace présentée ci-dessous :
symfony console make:entity
Class name of the entity to create or update (e.g. BraveKangaroo): > Recommendation created: src/Entity/Recommendation.php created: src/Repository/RecommendationRepository.php Entity generated! Now let's add some fields! You can always add more fields later manually or by re-running this command. New property name (press <return> to stop adding fields): > recommendation Field type (enter ? to see all types) [string]: > Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: > updated: src/Entity/Recommendation.php Add another property? Enter the property name (or press <return> to stop adding fields): > film Field type (enter ? to see all types) [string]: > ? Main types * string * text * boolean * integer (or smallint, bigint) * float Relationships / Associations * relation (a wizard 🧙 will help you build the relation) * ManyToOne * OneToMany * ManyToMany * OneToOne Array/Object Types * array (or simple_array) * json * object * binary * blob Date/Time Types * datetime (or datetime_immutable) * datetimetz (or datetimetz_immutable) * date (or date_immutable) * time (or time_immutable) * dateinterval Other Types * json_array * decimal * guid Field type (enter ? to see all types) [string]: > relation What class should this entity be related to?: > Film What type of relationship is this? ------------ -------------------------------------------------------------------------- Type Description ------------ -------------------------------------------------------------------------- ManyToOne Each Recommendation relates to (has) one Film. Each Film can relate to (can have) many Recommendation objects OneToMany Each Recommendation can relate to (can have) many Film objects. Each Film relates to (has) one Recommendation ManyToMany Each Recommendation can relate to (can have) many Film objects. Each Film can also relate to (can also have) many Recommendation objects OneToOne Each Recommendation relates to (has) exactly one Film. Each Film also relates to (has) exactly one Recommendation. ------------ -------------------------------------------------------------------------- Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]: > ManyToOne Is the Recommendation.film property allowed to be null (nullable)? (yes/no) [yes]: > no Do you want to add a new property to Film so that you can access/update Recommendation objects from it - e.g. $film->getRecommendations()? (yes/no) [yes]: > A new property will also be added to the Film class so that you can access the related Recommendation objects from it. New field name inside Film [recommendations]: > Do you want to activate orphanRemoval on your relationship? A Recommendation is "orphaned" when it is removed from its related Film. e.g. $film->removeRecommendation($recommendation) NOTE: If a Recommendation may *change* from one Film to another, answer "no". Do you want to automatically delete orphaned App\Entity\Recommendation objects (orphanRemoval)? (yes/no) [no]: > yes updated: src/Entity/Recommendation.php updated: src/Entity/Film.php Add another property? Enter the property name (or press <return> to stop adding fields): > Success! Next: When you're ready, create a migration with make:migration
Notez que l’assistant nous demande Do you want to add a new property
to Film so that you can access/update Recommendation objects from
it. Comme nous avons validé l’option par défaut yes, il génère une
propriété « inverse » pour notre association ManyToOne, dans la classe Film
, qu’il nomme
recommendations
(notez le « s » à la fin). Elle permettra d’accéder à
la collection des recommendations d’un film.
Quant à la déclaration du paramètre orphanRemoval
, elle permet de gérer le fait
qu’un film est une entité maître dans la relation, et que les
recommendations sans film n’ont pas lieu d’exister (on reverra cela plus tard).
Rechargez si nécessaire le projet dans l’IDE pour voir apparaître le code des nouvelles classes PHP :
src/Entity/Recommendation.php
src/Repository/RecommendationRepository.php
mais aussi les modifications effectuées dans src/Entity/Film.php
3.6. TODO Étape 2-d : Observation du code PHP objet généré
Sans surprise, la classe Film
du modèle de données comporte deux
propriétés primaires (mono-valuées) :
title
et year
(et leurs getters et setters), mais aussi une
propriété id
nécessaire au mapping avec les entités de la base de
données (la valeur de la clé primaire de la table SQL).
3.6.1. Attributs PHP pour Doctrine
Remarquez que le code de ces propriétés de classes
est « décoré » par des attributs PHP préfixés par ORM
(#[ORM\...]
). Ces
attributs permettent d’enrichir la
sémantique du langage PHP « de base », en ajoutant des « méta-données » aux
classes ou à leurs propriétés, méthodes, etc. Ainsi, pour la
propriété $id
identifiant les instances, ce code permet à Doctrine d’identifier qu’il s’agit de gérer
une clé primaire dans la base de données, en lui ajoutant l’attribut
ORM\Id
. Cela permet de rendre la classe Film
utilisable dans Doctrine,
de gérer la persistence des instances de films dans la base de données
relationnelle.
3.6.2. Accès aux propriétés mono-valuées
Dans les programmes on va pouvoir utiliser des getters et setters « classiques », pour
les propriétés mono-valuées comme title
, par exemple afficher le titre
d’un film : print $film->getTitle();
.
3.6.3. Accès aux propriétés multi-valuées
Comme on l’a remarqué à l’appel de make:entity
, la classe Film
comporte aussi une propriété multi-valuée recommendations
qui
agit comme une collection des instances de Recommendation
relatives
à une instance de Film
. Remarquez que l’assistant générateur de code
nous a proposé de la nommer ainsi, en ajoutant un « s » au nom de la
classe, pour bien matérialiser le pluriel. Cette propriété comporte l’attribut
ORM\OneToMany
qui définit en particulier l’entité cible de
l’association 1-n (targetEntity: Recommendation::class"
).
Elle se manipule en PHP objet via les méthodes d’une interface
Collection
, de façon semblable aux
tableaux PHP classiques (Array
).
Plutôt que d’utiliser des getters et setters classiques, comme pour
les propriétés mono-valuées, il s’agit de gérer le contenu d’une
collection. Ainsi le générateur de code a généré pour Film::recommendations
plusieurs
méthodes : getRecommendations(): Collection
,
addRecommendation(Recommendation $recommendation)
et removeRecommendation(Recommendation $recommendation)
.
Exemple de boucle énumérant les recommendations d’un film :
$recommendations = $film->getRecommendations(); if (count($recommendations)) { foreach($recommendations as $recommendation) { print $recommendation; } }
Il se peut que vous constatiez des erreurs ou warnings, ou des comportements étranges dans Eclipse, qui peuvent s’expliquer dans la mesure où les générateurs de code travaillent « dans son dos ». Dans ce genre de cas, n’hésitez pas à sélectionner le menu « Project > Clean », qui aide parfois à résoudre ce genre de comportements erratiques.
3.7. TODO Étape 2-e : Création de la base de données SQLite
Procédez maintenant à la création du fichier de stockage de la base de données SQLite :
symfony console doctrine:database:drop --force symfony console doctrine:database:create
Créez maintenant le schéma de la base de données :
symfony console doctrine:schema:create
Le schéma de notre base de données a été créé par Doctrine à partir des directives de notre code PHP objet.
Les tables et index correspondant aux classes de notre modèle de données sont créées. Les tables restent vides en attendant qu’on y ajoute des données.
On reverra un peu plus tard en détail ce qui se cache derrière ces invocations, et l’impact dans la base SQLite.
Mais allons plutôt voir comment mettre des données dans la base, pour l’instant.
3.8. TODO Étape 2-f : Ajout du chargement de données de test
Le module DataFixtures de Symfony permet d’utiliser dans le code des mécanismes chargement de données de test. Cela permet aux développeurs de tester le code de leur modèle de données, en local, en chargeant des données dans la base SQLite.
On va donc l’utiliser pour peupler la base de données avec des données de films ou de recommendations (qui vous rappelleront quelques souvenirs).
Une classe AppFixtures
a été générée par Composer au moment de l’ajout
du module DataFixtures au projet. Nous n’avons plus qu’à compléter le code de cette classe.
Copiez-collez le code ci-dessous de façon à remplacer le contenu du fichier source src/DataFixtures/AppFixtures.php
.
Les commentaires dans le source devraient vous permettre de facilement
identifier la nature des données chargées dans la base, même si on
utilise des mécanismes pas forcément limpides aujourd’hui (générateurs
avec l’instruction PHP yield
).
Le cœur de l’opération se passe dans les instructions suivantes (de façon similaire à ce qu’on aurait fait en Java) :
- création d’instances en mémoire (crées avec
new Film()
, ounew Recommendation()
) - appels aux méthodes setters des propriétés pour initialiser les données mono-valuées
- ajout des nouvelles instances de
Recommendation
dans la collection des recommendations du film correspondant.
On étudiera plus tard persist()
et flush()
sont des méthodes du gestionnaire de
données Doctrine, et qui ont pour effet de
sauvegarder les données présentes en mémoire dans la base.
<?php namespace App\DataFixtures; use App\Entity\Film; use App\Repository\FilmRepository; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use App\Entity\Recommendation; class AppFixtures extends Fixture { /** * Generates initialization data for films : [title, year] * @return \\Generator */ private static function filmsDataGenerator() { yield ["Evil Dead", 1981]; yield ["Evil Dead", 2013]; yield ["Fanfan la Tulipe", 2003]; yield ["Fanfan la Tulipe", 1952]; yield ["Mary a tout prix", 1998]; yield ["Black Sheep", 2008]; yield ["Le Monde de Nemo", 2003]; } /** * Generates initialization data for film recommendations: * [film_title, film_year, recommendation] * @return \\Generator */ private static function filmRecommendationsGenerator() { yield ["Evil Dead", 1981, "Ouh ! Mais ça fait peur !"]; yield ["Evil Dead", 2013, "Même pas peur !"]; yield ["Evil Dead", 2013, "Insipide et sans saveur"]; yield ["Fanfan la Tulipe", 1952, "Manque de couleurs"]; yield ["Fanfan la Tulipe", 1952, "Super scènes de combat"]; yield ["Fanfan la Tulipe", 2003, "Mais pourquoi ???"]; yield ["Mary a tout prix", 1998, "Le meilleur film de tous les temps"]; yield ["Black Sheep", 2008, "Un scenario de génie"]; yield ["Black Sheep", 2008, "Une réalisation parfaite"]; yield ["Black Sheep", 2008, "À quand Black Goat ?"]; } public function load(ObjectManager $manager) { $filmRepo = $manager->getRepository(Film::class); foreach (self::filmsDataGenerator() as [$title, $year] ) { $film = new Film(); $film->setTitle($title); $film->setYear($year); $manager->persist($film); } $manager->flush(); foreach (self::filmRecommendationsGenerator() as [$title, $year, $recommendation]) { $film = $filmRepo->findOneBy(['title' => $title, 'year' => $year]); $reco = new Recommendation(); $reco->setRecommendation($recommendation); $film->addRecommendation($reco); // there's a cascade persist on film-recommendations which avoids persisting down the relation $manager->persist($film); } $manager->flush(); } }
Sauvegardez cette modification, et testez l’exécution du chargement des données de test :
symfony console doctrine:fixtures:load -n
En principe, à ce stade, la commande affiche des erreurs (attendues) du type :
In ORMInvalidArgumentException.php line 105: Multiple non-persisted new entities were found through the given association graph: * A new entity was found through the relationship 'App\Entity\Film#recommendations' that was not configured to cascade persist operations for entity: App\Entity\Recommendation@000000003e5fd2a800000 0003284ac56. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade ={"persist"}). If you cannot find out which entity causes the problem implement 'App\Entity\Recommendation#__toString()' to get a clue.
Les assistants générateur de code nous facilitent beaucoup le travail, mais ils ne font quand même pas tout à notre place.
Le problème qui se pose est que l’assistant générateur de code
make:entity
n’a pas pu prendre toutes les décisions nécessaires, pour
l’association 1-N qui lie les entités Film et Recommendation comme
nous le souhaiterions. Il y a un problème de propagation en cascade
des ajouts de données.
On va corriger ce problème dans l’étape suivante.
Si vous lisez attentivement le message d’erreur ci-dessus, vous remarquerez cependant que Symfony nous donne quand même des recommendations utiles : le message d’erreur suggère des pistes de correction (« To solve this issue… »), comme souvent avec Symfony, pour des erreurs fréquentes rencontrées par les développeurs.
Ne vous inquiétez pas, nous reverrons en détail tous ces éléments dans les séquences suivantes du cours, mais pour l’instant, suivez le présent support : on n’est plus très loin du but !
3.9. TODO Étape 2-g : Ajout de la persistence en cascade
Appliquons la suggestion donnée par le message d’erreur ci-dessus.
Ajoutez cascade: ["persist"]
dans les options de l’attribut PHP
#[ORM\OneToMany]
qui décore Film::recommendations
, dans le fichier source
src/Entity/Film.php
. Le code devient :
#[ORM\OneToMany(targetEntity: Recommendation::class, mappedBy: 'film', orphanRemoval: true, cascade: ["persist"])] private Collection $recommendations;
Relancez le chargement des données de test, qui va marcher, cette fois :
symfony console doctrine:fixtures:load -n > purging database > loading App\DataFixtures\AppFixtures
3.10. TODO Étape 2-h : Vérification du contenu de la base de données
Il n’y a plus qu’à vérifier le contenu de la base de données SQLite
présente dans notre projet (dans var/data.db
), avec un peu de SQL :
symfony console dbal:run-sql 'SELECT * FROM film'
Idem pour la table recommendation
:
symfony console dbal:run-sql 'SELECT * FROM recommendation where film_id=2'
Vous pourriez tout aussi bien utiliser
SQLiteBrowser / BBrowser for SQLite et consulter le contenu du fichier
$HOME/CSC4101/tp-mini-allocine/miniallocine/var/data.db
Et voilà, la couche d’accès aux données fonctionne. Ajoutons maintenant la présentation en ligne de commande.
4. Étape 3 : Ajout de l’interface console
Cette étape va permettre d’ajouter une couche de présentation (dans le terminal) dans notre application.
Pour l’instant on va reproduire à peu près ce qui existait dans l’application en Java étudiée dans CSC3101, avec une interaction en ligne de commande, dans un terminal.
4.1. TODO Étape 3-a : Ajout de la commande app:list-films
Ajoutons une interface en ligne de commande à notre application PHP,
pour disposer d’une commande accessible pour le développeur via
l’interface principale offerte par bin/console
.
Cette fois encore, utilisons un assistant générateur de code pour nous faciliter le travail :
symfony console make:command
Choose a command name (e.g. app:orange-pizza): > app:list-films created: src/Command/ListFilmsCommand.php Success! Next: open your new command class and customize it! Find the documentation at https://symfony.com/doc/current/console.html
Consultez le résultat généré dans src/Command/ListFilmsCommand.php
Vérifiez que la commande est bien disponible via :
symfony console list app
et :
symfony console app:list-films --help
Et enfin qu’elle répond quand on l’invoque :
$ symfony console app:list-films [OK] You have a new command! Now make it your own! Pass --help to see your options.
4.2. TODO Étape 3-b : Branchement de la commande à la base de données
Pour que notre commande puisse afficher la liste des films on va devoir réaliser quelques branchements utiles.
Modifiez le code de src/Command/ListFilmsCommand.php
, pour ajouter
une propriété filmRepository
dans la classe ListFilmsCommand
, permettant
de gérer l’accès à la base de données via le composant Doctrine, et
son initialisation.
Copiez-collez les éléments ci-dessous pour ajouter la propriété et le constructeur dans ListFilmsCommand
(attention à ne pas
dupliquer la déclaration de la classe, et à ne pas oublier les use
) :
// ... use App\Entity\Film; use App\Repository\FilmRepository; use Doctrine\Persistence\ManagerRegistry; // ... class ListFilmsCommand extends Command { private ?FilmRepository $filmRepository; public function __construct(ManagerRegistry $doctrineManager) { $this->filmRepository = $doctrineManager->getRepository(Film::class); parent::__construct(); } //...
Ce repository $filmRepository
permet désormais de charger des
instances d’objets de Film
à partir de la base de données.
C’est le repository Doctrine pour la classe Film
obtenu via
getRepository(Film::class)
. Ce repository est en fait une classe qui a été générée par
l’asssistant make:entity
que nous avions lancée un peu plus tôt (« created: src/Repository/FilmRepository.php »)
4.3. TODO Étape 3-c : Ajout de l’affichage de la liste des films
Ajoutons maintenant dans la commande le code permettant de charger tous les films présents dans la base de données.
On utilise la méthode findAll()
du repository
Doctrine des films, qui renvoie un
tableau de films. On peut manipuler ce tableau comme un tableau PHP
ordinaire, par exemple avec foreach
.
Modifiez la méthode execute()
de ListFilmsCommand
:
protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $films = $this->filmRepository->findAll(); if (! $films) { $io->error('no films found!'); return Command::FAILURE; } else { $io->title('list of films:'); foreach ($films as $film) { $io->text($film); } return Command::SUCCESS; } }
Testez avec :
symfony console app:list-films
Le programme échoue avec une erreur (attendue) du type :
$ symfony console app:list-films list of films: ============== In SymfonyStyle.php line 116: Symfony\Component\Console\Style\SymfonyStyle::text(): Argument #1 ($message) must be of type array|string, App\Entity\Film given, called in /.../miniallocine/src/Command/ListFilmsCommand.php on line 51 app:list-films [--option1] [--] [<arg1>]
Ce genre de chose devrait vous paraître cohérent avec ce qui a été vu l’année passée en programmation orientée objet.
La méthode text()
essaie d’afficher une chaîne de caractères sur
la console… mais une instance de la classe Film
peut-elle être
convertie en chaîne de caractères ? Pas de base, en PHP.
Réglons cela.
4.4. TODO Étape 3-d : Ajout de Film::__toString()
Ajoutez dans src/Entity/Film.php
la méthode de conversion de film en chaîne de caractère qui
nous manque : Film::__toString()
, par exemple :
public function __toString() { return $this->getTitle() . " (" . $this->getYear() . ")"; }
Ré-essayez le lancement de notre commande :
$ symfony console app:list-films list of films: ============== Evil Dead (1981) Evil Dead (2013) Fanfan la Tulipe (2003) Fanfan la Tulipe (1952) Mary a tout prix (1998) Black Sheep (2008) Le Monde de Nemo (2003)
Bravo, vous avez une application PHP objet qui effectue des requêtes en base de données et affiche le résultat sur la console. La classe !
Vous pouvez éventuellement l’améliorer encore un peu, en remplaçant l’itération sur les éléments de la collection des films, par un affichage sous forme de tableau, directement :
Remplacez :
foreach ($films as $film) { $io->text($film); }
directement par $io->listing($films);
(le module d’affichage fourni
par Symfony propose directement ce qu’il nous faut avec une mise en
forme standard) :
$ symfony console app:list-films list of todos: ============== * Evil Dead (1981) * Evil Dead (2013) * Fanfan la Tulipe (2003) * Fanfan la Tulipe (1952) * Mary a tout prix (1998) * Black Sheep (2008) * Le Monde de Nemo (2003)
Pas beau ? !
5. Étape 4 : Chargement de données liées dans une association OneToMany
L’objectif de cette étape est d’expliquer comment le code permet de naviguer dans les associations entre entités de notre modèle de données, en parcourant une association OneToMany.
Si vous avez terminé l’étape précédente, tout va bien : vous avez réalisé une première application PHP utilisant Symfony, et qui est capable de s’interfacer avec une base de données relationnelles, pour charger des données.
Allons plus loin pour progresser un peu dans notre connaissance du modèle de données objet de PHP, qu’on met en œuvre avec Doctrine, dans les applications Symfony.
Cette fois, nous fournissons un peu moins d’aide pour diminuer le copier/coller et vous laisser retrouver les opérations par vous-même.
5.1. TODO Étape 4-a : Ajout de l’affichage des recommendations d’un film
Nous allons parcourir les données de notre application en utilisant l’approche objet, pour charger des données liées, afin d’afficher les recommendations de chaque film.
En base de données, les films et les recommendations sont stockés dans
des tables séparées (avec une clé étrangère migrée de films
vers
recommendations
pour matérialiser l’association 1-N).
Comment allons-nous gérer cela en PHP ?
Plutôt que de charger en mémoire les films, d’un côté, puis les recommendations de l’autre, et de « réconcilier » ensuite manuellement quelles recommendations correspondent à quel film, nous allons exploiter les méthodes de parcours des propriétés de type collection offertes par l’ORM Doctrine.
Si on accède à la propriété Film::recommendations
via
$film->getRecommendations()
(déjà introduit brièvement plus haut), on
programme en objet tout naturellement… et Doctrine fera les
chargements correspondants sous-le capot sans qu’on ait à s’en
préoccuper (jointures, etc.).
Mettons cela en pratique :
- ajoutez, comme précédemment, une nouvelle commande
app:show-film
qui affiche un film et ses recommendations, en utilisant le générateurmake:command
. - Modifiez le code généré pour :
- brancher la base de données (comme ci-dessus, pour le repository des films)
- gérer le passage d’arguments à la commande : cette commande
attend deux arguments : titre et année du film recherché
(cf.
configure()
ci-dessous). - afficher les données sur la console
Vous pouvez vous inspirer du code ci-dessous pour l’affichage des recommendations d’un film :
<?php // [...] #[AsCommand( name: 'app:show-film', description: 'Show recommendations for a film', )] class ShowFilmCommand extends Command { //... protected function configure() { $this ->addArgument('title', InputArgument::REQUIRED, 'Title of the film (spaces must be quoted)') ->addArgument('year', InputArgument::REQUIRED, 'Year of the film') ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $title = $input->getArgument('title'); $year = $input->getArgument('year'); if ($year && ! preg_match('/^\d+$/', $year)) { $io->error('second argument must be integer'); return Command::FAILURE; } $film = $this->filmRepository->findOneBy([ 'year' => $year, 'title' => $title]); if(!$film) { $io->error('unknown film: ' . $title . ' (' . $year .')'); return Command::FAILURE; } $io->title($film . ':'); $io->listing($film->getRecommendations()->getValues()); return Command::SUCCESS; } // [...] }
Testez :
symfony console app:show-film coin 42
Si vous avez une erreur par rapport à l’absence de
ShowFilmCommand::$filmRepository
, c’est que vous êtes allés un peu
vite en besogne (lisez précisément les consignes ci-dessus). Recopiez, depuis le code existant dans
ListFilmsCommand.php
, les éléments correspondant
à la propriété filmRepository
, et à son initialisation dans le
constructeur (cf. code de src/Command/ListFilmsCommand.php
vu plus haut).
Re-testez :
$ symfony console app:show-film coin 42 [ERROR] unknown film: coin (42)
Bravo !
Attention à bien protéger les arguments passés à la commande par le shell, via des quotes :
symfony console app:show-film "Black Sheep" 2008
Ça ne marche toujours pas ?
Peut-être faut-il modifier Recommendation
pour ajouter la méthode
__toString()
manquante, comme on l’avait fait pour
src/Entity/Film.php
. La méthode devrait être triviale à écrire en objet.
Finalement, ça doit marcher :
$ symfony console app:show-film "Black Sheep" 2008 Black Sheep (2008): =================== * Un scenario de génie * Une réalisation parfaite * À quand Black Goat ?
Bravo, vous savez maintenant comment accéder aux données, en programmant avec une approche objet en PHP, grâce Doctrine, y compris pour des données liées par des associations 1-N.
6. Étape 5 : Gestion de la base de données pour les tests pendant le développement
Cette étape vise à mieux expliciter comment les développeurs doivent gérer la base de donnée de tests pendant le développement
Comme on l’a vu plus haut, Doctrine permet 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.
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 étions passés un peu vite
sur des commandes « magiques » database:create
et schema:create
.
On va voir plus en détail comment ça fonctionne.
6.1. TODO Étape 5-a : Génération de la base de données SQLite
L’objectif de cette étape est d’expliciter comment les outils Doctrine pour le développeur gèrent la création de la base de données de tests.
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:create
:Commencez-donc par vérifier ce qui serait fait :
symfony console doctrine:schema:create --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 attributs#[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 nommés de façon arbitraire par Doctrine. Pour le reste : classes, propriétés mono-valuées, ça correspond normalement aux noms présents dans le code.
Essayez de créer le schéma pour de bon, avec :
symfony console doctrine:schema:create
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 (vraiment, cette fois).
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 dans le code des classes PHP (ajout d’une propriété, ajout d’une classe…). À chaque fois que l’on fait évoluer le modèle de
données dans les classes PHP de src/Entity/
, et qu’on veut tester
l’exécution, il faudra mettre à jour le contenu de la base de tests
SQLite. Il faut alors exécuter la commande doctrine:schema:update
,
pour mettre à jour le schéma, ou ré-créer toute la base de données de
zéro en relançant doctrine:schema:create
.
6.2. TODO Étape 5-b : Avantages de la gestion des données de tests via les Fixtures
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.
Revoyons plus en détails comment marchent les 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 qu’une fois les données de test chargées dans la base.
- Consultez le code des fixtures dans la classes PHP présente dans
src/DataFixtures/AppFixtures.php
(on vous avait fait ajouter ce code par copier/coller).
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 ?
Ce code de la classe AppFixtures
introduit quelques instructions peu connues
comme l’instruction PHP 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.
6.3. TODO Étape 5-c : Observation des Requêtes SQL de chargement
Sous le capot, Doctrine utilise des requêtes SQL SELECT pour le chargement des données.
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-mini-allocine/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.
6.4. TODO Étape 5-d : 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 « 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 dans 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…) ?
Examinons la documentation
6.5. TODO Étape 5-e : 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 (sans parler de demander à une IA).
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 6.4.x actuellement).
En cas de doute : symfony console about
.
Pour lire les documentations pertinentes, attention à ne pas regarder
celles trop vieilles (Symfony v5), mais pas trop récentes (Symfony
v7)…
Nous avons fait de notre mieux pour indiquer des liens vers les « bonnes versions », dans notre contexte :
Symfony 6.4.x, Doctrine 2.15, etc.
Méfiance en cas de recherches dans un moteur de recherche, dans les forums, etc.
6.5.1. Documentation des critères de requêtage avec Doctrine
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.
Bon, nous avons trouvé des exemples de requêtes avec critère de tri (et des filtres). Ça se passe au niveau du Repository, qui est chargé de faire tout ça.
6.5.2. Documentation du chargement dans le Repository d’entités
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.
You can think of a repository as a PHP class whose only job is to help you fetch entities of a certain class.
Si on creuse dans cette direction, la documentation Symfony du repository donne quelques indices sur la possibilité de faire des requêtes spécifiques : Querying for Objects: The Repository. En ajoutant des méthodes.
Ça tombe bien, nous avons justement une classe FilmRepository
qui a été créée par make:entity
dans src/Repository/FilmRepository.php
.
Ouvrons le code dans l’IDE. Dans le code de la classe, l’assistant make:entity
a généré des exemples
(désactivés), d’ajout de méthodes de chargement spécifiques.
Mais rien pour le chargement par défaut. Il n’a rien généré comme méthode findAll()
dans la classe.
Mais si on demande à Eclipse d’afficher la doc de findAll()
(en pointant dessus dans le code de ListFilmsCommand
par exemple), il nous affiche :
Doctrine\ORM\EntityRepository::findAll() : array
Finds all entities in the repository.
Specified by:
findAll() in ObjectRepository
Returns:
array object> The objects.
@psalm-return list The entities.
Si on accède au code de cette méthode dans Eclipse (depuis le popup qui affiche la doc, cliquer sur « F2 »), Eclipse
ouvre alors vendor/doctrine/orm/src/EntityRepository.php
.
C’est le code de la bibliothèque Doctrine, qui contient bien
le code de la méthode findAll()
de base que nous cherchons à redéfinir :
public function findAll(): array { return $this->findBy([]); }
Bingo. C’est juste un appel à findBy()
sans lui donner de critères, et donc par défaut, tout charger.
Le code de FilmRepository
commence par :
/** * @extends ServiceEntityRepository<Film> */ class FilmRepository extends ServiceEntityRepository
C’est donc que la methode findAll()
de base venait via ServiceEntityRepository
…
Eclipse propose d’afficher la documentation de cette classe quand on laisse la souris dessus. Ça nous affiche :
Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository
Optional
EntityRepository
base class with a simplified constructor (for autowiring).To use in your class, inject the « registry » service and call the parent constructor. For example:
class YourEntityRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, YourEntity::class); } }
Hmmm : « To use in your class… », OK, c’est bien ce qu’a fait le
générateur de code pour nous donner une classe FilmRepository
avec un
constructeur.
On pourrait aller essayer de trouver le code de la méthode findAll()
,
de cette classe de base… mais il est quelque part ailleurs avec le jeu des héritages de classes et d’implémentations d’interfaces… et on
rentre vite dans un jeu de piste et on se perd dans la complexité du framework. Heureusement l’IDE semble s’en sortir (voi ci-contre).
De tout cela, on peut maintenant déduire avec certitude qu’il serait
intéressant de surcharger le comportement par défaut de findAll()
et
réécrire la méthode pour changer ce comportement par défaut.
On a presque tout trouvé : il suffit de reprendre ce bout de code pour le mettre dans FilmRepository
, pourvu qu’on sache quoi passer en argument à findBy()
.
6.5.3. Détails d’utilisation de findBy()
Il nous faut écrire le code de la nouvelle méthode findAll()
; pour appliquer le bon
critère de chargement. Pour cela il faut savoir comment utiliser findBy()
et lui passer
les bons arguments, comme ce fameux critère de tri qui nous intéresse.
Si on veut creuser un plus loin, il faut lire la doc du projet Doctrine
lui-même.
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
On y trouve comme exemple :
The
EntityRepository#findBy()
method additionally accepts orderings, limit and offset as second to fourth parameters :$tenUsers = $em->getRepository('MyProject\Domain\User') ->findBy(array('age' => 20), array('name' => 'ASC'), 10, 0);
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
Si on résume, avec les exemples des deux documentations, on a 4
arguments possibles à findBy()
:
« matching criteria », comme dans :
$products = $repository->findBy(['name' => 'Keyboard'], ...
ou
...->findBy(array('age' => 20), ...
« orderings […] as second […] parameter » : comme dans
...->findBy(['name' => 'Keyboard'], ['price' => 'ASC']);
et
...->findBy(array('age' => 20), array('name' => 'ASC'), ...
« limit » comme dans :
...->findBy(array('age' => 20), array('name' => 'ASC'), 10, ...
« offset as […] fourth parameters » comme dans :
...->findBy(array('age' => 20), array('name' => 'ASC'), 10, 0);
Il faut quand même être perspicace et savoir ce qu’on cherche si on a une idée de comment marchent les requêtes en base de données, d’autant que les exemples utilisent des syntaxes différentes pour les tableaux PHP (raisons historiques…).
6.5.4. Explication finale après avoir lu la doc
Tout devrait être plus clair maintenant sur ce qu’on a fait dans l’étape « Modification de l’ordre de tri des films par défaut ».
Vous ppouvez maintenant mieux comprendre pourquoi l’appel à findBy()
est codé comme cela dans la redéfinition de
FilmRepository::findAll()
:
return $this->findBy( array(), array('year' => 'ASC', 'title' => 'ASC') );
premier argument (obligatoire) : « matching criteria ». C’est le critère de filtre du
WHERE
de la requête SQL.On passe un tableau vide :
array()
. Vu que c’est un tableau vide, il n’y a donc pas de filtre à appliquer. C’est correct, on veut toutes les instances.deuxième argument (optionnel) : « ordering ». C’est le critère de tri qu’on souhaite : (par
year,
puis partitle
).On passe le tableau associatif :
array('year'=>'ASC', 'title'=>'ASC')
.
Le code s’éclaircit, mais n’est pas super élégant.
Pourquoi ne pas passer seulement un argument contenant le critère de
tri, et devoir passer un tableau vide un peu étrange ? … C’est comme
ça en PHP : findBy()
attend un critère de recherche/filtre obligatoire comme
premier argument, et l’API de Doctrine a été faite comme ça. De plus, PHP ne
fournissait probablement pas un moyen de nommer les arguments
attendus, pour simplifier la tâche du programmeur… Qu’il en soit
ainsi : on peut passer un tableau vide en argument, et ça marche.
D’où cet appel un peu mystérieux de prime abord.
En conclusion, la documentation est importante, mais parfois insuffisante… Les outils de l’IDE aident souvent, et le code aide aussi parfois.
7. Étape 6 : Ajout de fonctions de modification des données
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 qui gère chacune des commandes.
7.1. TODO Étape 6-a : Commande d’ajout de films
7.1.1. Génération du code de la commande app:add-film
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
7.1.2. Ajout de la méthode save()
au repository
Ajoutez la méthode FilmRepository::save()
, à la classe FilmRepository
,
sur le modèle suivant :
public function save(Film $film, bool $flush = false): void { $this->getEntityManager()->persist($film); if ($flush) { $this->getEntityManager()->flush(); } }
7.1.3. Ajout du code sauvegardant le nouveau film
Voici un squelette de code à adapter, qui permet d’ajouter des
données dans la base via Doctrine, dans la méthode AddFilmCommand::execute()
:
use Symfony\Component\DependencyInjection\ContainerInterface; use Doctrine\ORM\EntityManager; use App\Entity\Film; //... class AddFilmCommand //... { //... protected function execute(InputInterface $input, OutputInterface $output): int { //... // crée une instance en mémoire $film = new Film(); $film->setTitle($title); $film->setYear($year); // sauve l'instance dans la base de données $this->filmRepository->save($film, true); // si tout va bien, on récupère un identifiant if($film->getId()) { $io->text('Created: '. $film); return Command::SUCCESS; } else { $io->error('could not create film!'); return Command::FAILURE; } } }
Examinez les commentaires du code ci-dessus, qui devraient déjà vous donner suffisamment d’informations pour arriver à faire marcher cette commande. Il manque volontairement tout l’aspect de configuration de la commande, déjà vu dans les exemples de code précédents : bon copier-coller et adaptations.
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é" 2023
Vérifiez-bien que vous avez bien obtenu les requêtes SQL INSERT
désirées dans les logs.
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
7.2. TODO Étape 6-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équence.
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, mais notez que l’on avait déjà vu des choses de ce genre dans le code des Fixtures) :
// Chargement en mémoire d'un film existant dans la base $film = $this->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 à
$this->filmRepository->save($film, true)
, etc.).
Testez de-même en ligne de commande :
symfony console app:add-recommendation "Mon film préféré" 2023 "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 $this->filmRepository->save($film, true)
,
donc en demandant à sauver le film,
alors que le nouvel objet (dont les données sont à sauvegarder, avec un INSERT)
, est la
recommendation. Normal, on a défini de la cascade.
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 une séquence 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 annotation Doctrine#[ORM\OneToMany]
- puis nous avions configuré explicitement l’option
cascade: ["persist"]
(« 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
), via l’appel à save()
du Repository.
Le fait qu’on ait utilisé Film::addRecommendation($recommendation)
sur $film
,
puis FilmRepository::save()
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.
7.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éflé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.
8. Ajout d’un contrôleur Web ?
Mais au fait… et le Web ? La ligne de commande sur le terminal, c’est bien beau… mais…
Le TP en Java de CSC3101 avait comporté une interface Web, accessible en HTTP… alors, on est dans le cours sur le Web, et on s’arrête à la ligne de commande ?
Patience.
Nous passerons à l’étude des contrôleurs pour la gestion d’HTTP dans Symfony dans les prochaines séquences du cours.
9. Évaluation
À l’issue de cette séance vous avez travaillé sur les éléments suivants :
- vous avez mieux compris le principe du modèle de données permettant de gérer en mémoire des objets (classes objet gérant entités du modèle de données avec Doctrine)
- vous avez utilisé les générateurs de code facilitant l’écriture des programmes
- vous savez initialiser la base de données avec des données de test (DataFixtures)
- vous savez parcourir les données liées, en PHP, dans le modèle de données objet
- vous savez faire des requêtes en base de donnée y compris en modification
- vous savez où trouver la documentation de Doctrine dans la doc Symphony
- vous savez comment observer les requêtes SQL sous-jacente, dans les logs d’exécution
- vous savez ajouter une interface en ligne de commande
Aller plus loin : Amélioration des fonctionnalités (optionnel)
On peut souhaiter tester différentes améliorations du code, que nous listons ici, mais qui sont optionnelles, pour aller plus loin, si vous avez terminé le reste du travail.
Cela permet de revoir quelques notions liées aux requêtes dans les bases de données relationnelles, comme la sélection, et le tri.
Affichage des films d’une année
Vous pouvez tester l’ajout de l’argument year
à app:list-films
pour ne lister que les
films d’une année donnée (filtre/sélection)
Configuration des arguments et options
Modifiez ListFilmsCommand::configure
pour permettre de filtrer
protected function configure() { $this //->setDescription('List films') ->addArgument('year', InputArgument::OPTIONAL, 'Filter films of a single year') ; }
Référez-vous au code de show-film
que vous avez ajouté précédemment,
pour voir un exemple de l’accès aux valeurs des arguments des
commandes, via les méthodes de la classe InputInterface
.
Filtrage des films au chargement
On peut utiliser ceci pour charger les films selon un critère de filtre :
$films = $this->filmRepository->findBy( ['year' => $year], ['title' => 'ASC']);
Le premier tableau passé en argument de findBy()
définit les
critères du filtre. Notez qu’on a la possibilité de définir plusieurs
critères, comme dans l’appel à findOneBy()
présent dans
app:show-film
.
Le deuxième argument de findBy()
permet de spécifier un critère de
tri (optionnel), ici les titres par ordre croissant (ASCending).
Dédoublonage des remakes
On peut souhaiter ne lister les titres des films qu’une seule fois, quand il y a eu des remakes.
Configuration des arguments et options
Ajoutez par exemple une option --unique
à app:list-films
:
protected function configure() { $this //->setDescription('List films') //... ->addOption('unique', null, InputOption::VALUE_NONE, 'Avoid listing remakes') ; }
Cf. Using Command Options pour la documentation sur la façon de s’en servir dans une commande.
Filtrage unique des titres
Programmez un algorithme simple permettant de supprimer les occurrences des multiples titres. Adaptez par exemple ce qui suit :
// tableau PHP standard $titles = array(); foreach($films as $film) { // ajout de valeurs au tableau $titles[] = ... } // filtre pour ne garder que des valeurs uniques du tableau print array_unique($titles);
On s’appuie sur la fonction
array_unique()
de la bibliothèque standard de PHP, « array_unique
— Removes
duplicate values from an array », dont vous pouvez parcourir le manuel
sur php.net
, comme pour toutes les autres fonctions de base.
Cette approche du dé-doublonage ne semble pas très élégante, et on
préfèrera probablement utiliser plutôt une méthode de chargement des
données alternative à mettre en œuvre dans le repository de Film
(du
style findOriginalFilms()
), qui
s’appuiera sur la bonne requête en base de données pour faire cela
bien plus efficacement. On trouvera plus de documentation sur le sujet
dans Querying for Objects: The Repository.
DONE Annexe
DONE Exemple d’implémentation de AddRecommendationCommand::execute()
Proposition d’implémentation de AddRecommendationCommand::execute()
pour l’ajout des entités liées dans une relation OneToMany :
// [...] class AddRecommendatioCommand extends Command { // [...] protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $title = $input->getArgument('title'); $year = $input->getArgument('year'); $recotext = $input->getArgument('recotext'); if ($year && ! preg_match('/^\d+$/', $year)) { $io->error('second argument must be integer'); return Command::FAILURE; } $film = $this->filmRepository->findOneBy([ 'year' => $year, 'title' => $title]); if(!$film) { $io->error('unknown film: ' . $title . ' (' . $year .')'); return Command::FAILURE; } // 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); $this->filmRepository->save($film, true); return Command::SUCCESS; }