Tutorial Jobeet pour Symfony 4 - Partie 3A: Le modèle de données
Maintenant que l’aspect fonctionnel de notre projet a été défini, nous allons pouvoir créer le modèle de données associé à notre application, c’est-à-dire les classes qui permettront d’interagir avec la base de données. Nous allons pour cela avoir recours à un ORM. Ce sera également l’occasion de voir comment ajouter et configurer un module tier dans notre projet.
D’après les scénarios que nous avons précédemment écrits, voici le schéma correspondant aux relations entre entités que l’on peut en déduire :
En plus des informations, nous avons également ajouté un champ created_at
et updated_at
à certaines tables afin de conserver une trace des dernières modifications des données que nous allons traiter.
Pour stocker les informations de l’application, nous allons utiliser une base de données relationnelle. Symfony étant un framework orienté-objet, nous allons donc manipuler les informations sous la forme d’objet. Les informations de notre base de données doivent ainsi être mappées avec notre modèle objet et pour cela nous allons utiliser un ORM.
Symfony 4 laisse le choix au développeur sur les outils qu’il souhaite utiliser. Contrairement aux versions précédentes, Symfony est livré “vide” et n’impose aucun choix par défaut. Pour ce tutorial, nous allons faire le choix d’utiliser l’ORM Doctrine qui est l’ORM le plus répandu dans l’écosystème PHP. Mais il serait tout à fait possible d’utiliser une simple connexion PDO ou Doctrine DBAL, ou un autre ORM tel que Propel, Eloquent, Pomm ou n’importe quel autre outil.
Avant de créer et configurer les classes de notre modèle de données, nous allons commencer par télécharger Doctrine. Pour intégrer ce dernier dans notre projet Symfony, nous devrons récupérer deux dépendances. La première, doctrine/orm
est l’ORM en tant que tel. La seconde dépendance consiste à intégrer l’ORM dans notre architecture Symfony. Symfony facilite le développement et la réutilisation de module que l’on peut partager entre plusieurs projets. Ces modules (des plugins en quelque sorte) sont appelés bundles dans l’écosystème du framework. Il convient donc de télécharger la dépendance doctrine/doctrine-bundle
qui va intégrer l’ORM dans notre environnement Symfony.
Afin d’éviter au développeur d’avoir à installer deux dépendances distinctes, les équipes de développement fournissent un méta-paquet Composer permettant d’installer les deux éléments d’un coup. Pour intégrer Doctrine à notre projet, il nous suffit donc de récupérer la dépendance symfony/orm-pack
.
Pour utiliser un bundle dans un projet, il est nécessaire d’activer ce dernier afin qu’il soit reconnu par le framework. Cette configuration s’effectue dans le fichier config/bundles.php
. Ce fichier retourne un tableau associatif où la clé des éléments correspond au namespace complet du bundle à activer et dont la valeur est également un tableau associatif indiquant les environnements pour lesquels le bundle est actif (ou non).
En réalité, l’activation d’un bundle se fait rarement manuellement. Effectivement, Symfony Flex, que nous avons évoqué brièvement lors de la mise en place du projet se chargera de cette action. Il sera néanmoins parfois nécessaire de corriger la configuration par défaut en activant ou désactivant le bundle pour certains environnements.
Maintenant que Doctrine est installé et activé dans notre projet, nous allons pouvoir commencer à paramétrer notre application pour qu’elle puisse accéder à une base de données. Pour cela nous allons renseigner les paramètres nécessaires à l’établissement de la connexion. Cette dernière étant propre à l’environnement d’exécution de notre code, nous allons définir les paramètres dans le fichier .env
. Ce dernier a d’ailleurs été prérempli avec une configuration de base par Flex :
Vous avez certainement noté la présence des fichiers
.env
et.env.dist
. Le fichier.env
est le fichier qui contient réellement la configuration de notre application. Contenant des informations pouvant être très sensibles (comme par exemple des mots de passe), il n’est pas versionné.
C’est pour cela qu’un fichier
.env.dist
est présent. Ce dernier qui lui est versionné sert de modèle pour que les développeurs qui vont être ammenés à travailler sur le projet puisse connaître les informations à renseigner.
L’exemple de configuration qui a été inséré par Symfony Flex permet de se connecter à une base de données MySQL. Pour les besoins de ce tutorial, ainsi que pour éviter l’installation d’un serveur, nous allons utiliser SQLite qui est un système de base de données ne nécessitant pas une architecture client-serveur. Il sera nécessaire de vérifier que le driver SQLite de PHP (php-sqlite3
) soit installé et activé. Nous allons ensuite modifier la variable d’environnement DATABASE_URL
comme suit :
Dans cette configuration, nous faisons référence à un paramètre
kernel.project_dir
définit par le framework (facilement reconnaissable car il est entouré du caractère%
). Ce dernier fait référence à la racine du dossier du projet et permet ainsi de définir facilement l’endroit où l’on souhaite enregistrer nos données.
Mise à jour du 23/09/2017 : En parcourant le projet sur Github, j’ai trouvé l’issue concernant ce problème ainsi que sa résolution. Tout devrait rentrer dans l’ordre lors de la publication de la branche 3.4 du projet.
Le dossier var
qui a été créé lors de la mise en place de Doctrine est, par convention, un dossier qui va contenir les fichiers écrits par notre application durant son fonctionnement (tel que des logs, des fichiers de cache, …). C’est donc tout naturellement dans ce dernier que nous allons stocker le fichier qui contiendra nos données SQLite.
Nous allons maintenant pouvoir démarrer l’écriture des classes qui vont modéliser nos données. Par défaut Doctrine est configuré pour travailler avec des annotations. Personnellement, je préfère séparer le code de sa configuration, c’est pour cela quand dans ce tutorial, nous utiliserons une configuration en YAML (il est également possible d’avoir une configuration en XML). Pour cela, nous allons éditer le fichier de configuration config/packages/doctrine.yaml
pour y mettre le contenu ci-dessous :
On retrouve dans cette configuration la variable %kernel.project_dir%
faisant référence au dossier racine de notre projet. Lorsqu’un paramètre de configuration est définit dans le framework. De la même façon, il est possible d’accéder à une variable d’environnement via la syntaxe %env(MA_VARIABLE)%
(comme dans le fichier Doctrine pour accéder à la chaine de connexion à la base de données).
Notons également la présence du paramètre env(DATABASE_URL)
permettant de définir le paramètre contenant la chaîne de connexion à notre base de données dans le cas où la variable d’environnement n’existerait pas.
Si vous analysez l’arborescence des fichiers, vous constaterez qu’il existe deux fichiers de configuration Doctrine :
config/packages/doctrine.yaml
etconfig/packages/prod/doctrine.yaml
.
Le premier est un fichier de configuration commun à tous les environnements. Il est ensuite possible de définir une configuration spécifique pour un environnement (défini par la variable
APP_ENV
du fichier.env
). Pour cela, il suffit de déposer le fichier de configuration dans un sous-dossier portant le nom de l’environnement pour lequel on souhaite surcharger la configuration et le framework le prendra automatiquement en compte.
Nous n’irons pas plus loin dans la configuration de Doctrine. Si vous après ce tutorial, vous souhaitez en savoir plus, je vous conseille de consulter la documentation officielle.
Créons maintenant les classes associées à notre modèle de données. Elles vont représenter les informations de la base de données sous la forme d’objets PHP (ces derniers sont appelés des entités) que l’on va pouvoir manipuler dans notre code. Lors de l’installation de Doctrine, Flex a ajouté un dossier src/Entity
dans lequel nous allons créer nos classes.
Les entités sont de simples objets PHP dont les propriétés vont correspondre aux champs de notre base de données. Commençons par la table la plus simple, la table category
:
Maintenant passons à la table job
. Un emploi étant lié à une catégorie, notre table contient une clé étrangère vers la catégorie associée. Dans notre entité, cette information va se modéliser sous la forme d’une propriété dont la valeur sera une instance de l’entité associée à la table catégorie (donc un objet Category
).
La table qui va gérer les informations d’affiliation est un peu plus complexe. Dans notre modèle, une société peut-être être affiliée à plusieurs catégories et une catégorie peut avoir des affiliations de plusieurs sociétés. Avec une base de données relationnelle, cela se traduit par la création d’une table d’association pour gérer cette information (il s’agit de la table CategoryAffiliate
).
Puisque nous avons dit qu’une table de notre base de données correspondait à un objet PHP, il devrait donc être nécessaire de créer deux nouveaux objets pour gérer cette relation. Mais en réalité cette table ne sert qu’à modéliser le fait qu’un objet Affialite
est rattaché à plusieurs objets Category
et vice-versa. Donc d’un point de vue programmation, un objet Affiliate
devrait avoir une propriété $categories
qui correspond à un tableau d’objet Affiliate
et l’entité Category
une propriété $affiliates
correspondant un tableau d’objet Affiliate
.
Notre ORM est tout à fait capable de gérer cette problématique. Nous allons donc créer notre objet Affiliate
avec une propriété $categories
que nous ferons correspondre à un tableau d’objet Category
. Doctrine gérera de manière automatique et transparente notre table d’association.
S’il avait été nécessaire de gérer des informations additionnelles, telles que par exemple la date de création de l’affiliation ou l’utilisateur ayant créé l’affiliation, il aurait été nécessaire de créer une entité supplémentaire et de gérer la relation manuellement.
Lors de la mise en place d’une relation où l’on va gérer un tableau d’objet, il est nécessaire d’initialiser la propriété en question avec une collection vide. Pour Doctrine, cela passe par la création d’un objet
ArrayCollection
comme dans l’exemple précédent.
N’oublions pas de modifier notre objet Category
pour y ajouter la propriété correspondant à nos objets Affialite
. Une catégorie étant également associée à plusieurs emplois, nous allons en profiter pour y ajouter la propriété correspondante.
Il est maintenant temps d’indiquer à Doctrine comment l’ORM va pouvoir faire le lien entre nos entités et les tables de la base de données. Pour cela, et comme nous l’avons spécifié précédement, nous allons placer des fichiers de configuration dans le dossier config/doctrine/mapping
. Tout comme pour l’écriture des classes, nous allons créer un fichier de configuration par entité en suivant la convention NomDeLaClasse.orm.yml
.
Les fichiers de configuration vont permettre d’indiquer à quelle table correspondent une entité et les différentes caractéristiques de nos propriétés (colonne de rattachement, type de données, contraintes d’intégrité, ….).
Commençons par la configuration de notre entité Category
:
Comme vous pouvez le constater, les propriétés de correspondant à des relations sont dissociés du reste des propriétés. On distingue quatre types de relation,
One-To-Many
(relation de type un vers plusieurs),Many-To-One
(plusieurs vers un),Many-To-Many
(relation de plusieurs à plusieurs) etOne-To-One
.
Voici la configuration de l’entité Job
:
Et pour finir le mapping correspondant à l’entité Affiliate
:
Lorsque nous allons enregistrer un emploi ou une affiliation, nous souhaiterions connaître la date de création et/ou de modification de la donnée écrite. Les entités correspondantes possèdent une propriété createdAt
et/ou updatedAt
. Plutôt que de devoir gérer manuellement cette information, nous allons déléguer ce travail à Doctrine.
En effet l’ORM possède un gestionnaire d’événement sur lequel nous allons nous brancher afin d’être notifié lors de l’enregistrement et la modification d’une entité. Nous allons donc ajouter cette configuration au mapping de nos entités afin d’indiquer la méthode de l’objet qui sera appelée lors de la propagation de l’événement.
Pour l’entité Job
:
Pour l’entité Affiliate
:
Il ne faudra pas oublier d’ajouter les méthodes correspondantes dans les classes associées. Ces dernières sont appelées avec un paramètre de type LifecycleEventArgs
contenant un certain nombre d’informations sur le contexte d’exécution de l’ORM.
Pour l’entité Affiliate
, nous souhaitons connaître uniquement la date de
création de la données.
Signalons l’existence d’un bundle
StofDoctrineExtensionBundle
contenant un ensemble d’extensions Doctrine pouvant être ajoutées à nos entités et possédant entre autre, une extensionTimestampable
. Cette dernière, permet de gérer de manière automatique les dates de création et de modification d’un entité sans avoir à ajouter manuellement les propriétés correspondantes.
Maintenant que nous avons indiqué à notre projet comment se connecter à notre base de données, créé nos entités et indiqué la configuration nécessaire à la liaison entre nos objets et le contenu de notre base, nous allons pouvoir initialiser cette dernière. Doctrine va encore nous faciliter le travail dans cette tâche car l’ORM est distribué avec des commandes qui vont nous assister dans ce travail.
Dans un terminal, nous allons exéctuer les commandes suivantes :
Vous pouvez constater que la base de données a été correctement initialiser en ouvrant le fichier contenant les données SQLite qui a été créé (
var/jobeet.db
) avec un outil tel que DB Browser for SQLite.
Voilà qui conclut notre section d’introduction au modèle de données. Nous avons maintenant une base de données (presque) prête à être utilisée et qui n’attends plus que nos données.
Retrouvez tous les tutorials Jobeet disponibles depuis le billet d’introduction de la série. Le code source de cette application est également disponible sur Github. Vous trouvez une branche associée à l’état du projet après chaque chapitre.