Contents
Chapitre 2: Faire un schema.yml avec des n:n
Derrière ce nom barbare se trouve un billet (réécrit) de mon blog – le seul traduit en français d’ailleurs puisque l’original était en anglais (pour information, je publie dans la langue de Shakespeare). Bien qu’il soit intéressant de comprendre les relations 1-1, 1-n, n-1, les n-n sont les plus compliquées à mettre en oeuvre pour le débutant, notamment quand se profile déjà le spectre des formulaires.
Donc, les many-to-many (plusieurs-à-plusieurs pour les puristes) apparaissent comme un défi, surtout quand vous commencez par ce que vous ne devez pas faire (sauf si vous n’avez pas le choix), c’est-à-dire, créer votre base de données et générer le fichier schema.yml à partir de là. Aussi, jouez le jeu, écrivez votre schéma à la main et non en copier/coller.
Le projet expliqué en 5 minutes
Je vais expliquer très rapidement le projet: nous allons faire une sorte de base de données de jeux, un peu à la Neoseeker ou à la Mobygames. En fait, peu importe ce qui va se passer pour l’utilisateur: il y aura trois catégories de membres privilégiés par exemple, la connexion se passe par une authentification du frontend, etc. etc..
La situation se présente comme telle: nous allons faire un site en anglais uniquement (l’internationalisation viendra dans un autre temps).
L’élément central est le jeu ayant comme caractéristiques:
- Un nom (nous utiliserons le nom anglais)
- Une date de sortie (bien qu’il y a plusieurs dates de sortie selon les plateformes, pour nous simplifier la vie, nous considérerons la plus ancienne – les prochains défis que je me fixe pour symfony seront justement l’amélioration de tout cela)
- Un éditeur (comme pour la date sortie, il s’agira d’un seul éditeur)
- Un développeur (id.)
- Une description (un background en anglais)
- Plusieurs genres
- Plusieurs plateformes
Bien évidemment, vous voyez déjà la relation n-n pointer le bout de son nez: un genre peut regrouper plusieurs jeux, tout comme une plateforme est alimentée par une librairie de jeux.
Pour continuer dans la description de notre projet, un genre a un nom, une plateforme également mais elle aura en plus un acronyme.
Voici donc les futures colonnes de nos tables, en excluant les ids qui seront générés par Doctrine. Notez que vous pouvez définir vos propres ids si vous le voulez, même si cela revient à écrire:
id: type: int(11) autoincrement: true
Faites attention néanmoins si vous avez des clés à déclarer: Doctrine génère les id en bigint(20), donc, veillez à respecter cette contrainte. Si vous déclarez les clés étrangères dans la table associative comme un int(4) par exemple, vous aurez un message d’erreur désagréable, sans pour autant que votre application soit bloquée. Mais avec l’utilisation de certaines commandes, vous risqueriez de devoir tout pas à pas.
En utilisant MySQL Workbench, voici le modèle de notre base de données:
Générer son schema.yml
Pour rappel, la syntaxe YAML est dépendante de l’indentation, donc, il est hors de question d’avoir une présentation du fichier de ce type:
id:
type: int(11)
primary: yes
Vous trouverez le fichier schema.yml dans le dossier config/doctrine.
Commençons par notre table centrale, la table Game:
Game: columns: name_game: type: string(255) notnull: true developer: type: string(100) default: Inconnu publisher: type: string(100) default: Inconnu background: type: string() release_date: type: date(25)
Bien que Game a une relation n-n avec Genre, je ne la précise pas dans ma table. Il suffit que je l’écrive dans la définition de la table Genre pour que Doctrine fasse le lien automatiquement.
Notez que j’assigne une valeur par défaut à Publisher et Developer et que le background correspond à une zone de texte.
Par contre, je ne mets pas de colonne created_at ou updated_at. La syntaxe pour ces dernières serait simplement d’ajouter:
Game: actAs: { Timestampable: ~ } colums: ....
Mais dans notre projet, vous ne devrez pas vous inquiétez de tout cela.
Passons maintenant à notre table Genre.
Genre: columns: name_genre: type: string(50) notnull: true
Jusqu’ici, rien de bien méchant et d’inconnu n’est apparu. C’est donc à la suite de ce code que vous pouvez ajouter la partie « relations ».
Genre: columns: name_genre: type: string(50) notnull: true relations: Games: foreignAlias: Genres class: Game refClass: GameGenre local: genre_id foreign: game_id
Donc, comment comprendre cette partie?
Je donne le nom de la relation en utilisant le nom de la table avec laquelle Genre est lié (Game au pluriel). Ensuite, je lui donne un alias optionnel qui correspond cette fois à ma table courante (à nouveau le nom au pluriel).
Class représente en fait la classe (la table) qui est liée à Genre.
Nous savons déjà que la représentation d’une relation n:n se fait traditionnellement avec une table intermédiaire (faites l’expérience dans MySQL Workbench et vous verrez la naissance de la table de liaison) où les relations seront stockées. Donc, nous indiquons dans notre schéma quelle table utiliser dans ce cas-ci: Doctrine saura que GameGenre stocke les clés primaires des jeux et des genres. Mais nous avons besoin d’un autre paramètre que nous verrons plus tard.
Local se réfère à l’objet courant, ici Genre, en donnant le nom de la colonne le concernant dans GameGenre (puisque refClasse est GameGenre). Foreign fait la même chose mais avec l’autre table.
Définissons maintenant GameGenre qui doit impérativement se trouver dans votre schéma, sinon,
Doctrine ne saura pas quoi faire.
GameGenre: columns: game_id: type: bigint(20) primary: true genre_id: type: bigint(20) primary: true relations: Genre: onDelete: cascade local: genre_id foreign: id foreignAlias: GameGenres Game: onDelete: cascade local: game_id foreign: id foreignAlias: GameGenres
Les explications sont similaires par rapport à Genre. Il y a trois différences:
- onDelete: cascade, qui est notre paramètre à donner afin de supprimer les lignes concernant soit un genre soit un jeu si celui-ci est supprimé.
- Le nom des tables concernées est le nom de la relation.
- Local représente toujours notre colonne de la table concernée mais Foreign ici prend le nom de la colonne dans la table conservée. C’est ainsi que si les clés primaires ont un nom autre que id, Doctrine sait faire le lien.
Une fois que vous savez cela, vous pourrez définir la relation entre Game et Platform:
Game: columns: name_game: type: string(255) notnull: true developer: type: string(100) default: Inconnu publisher: type: string(100) default: Inconnu background: type: string() release_date: type: date(25) Genre: columns: name_genre: type: string(50) notnull: true relations: Games: foreignAlias: Genres class: Game refClass: GameGenre local: genre_id foreign: game_id GameGenre: columns: game_id: type: bigint(20) primary: true genre_id: type: bigint(20) primary: true relations: Genre: onDelete: cascade local: genre_id foreign: id foreignAlias: GameGenres Game: onDelete: cascade local: game_id foreign: id foreignAlias: GameGenres Platform: columns: name_platform: type: string(50) notnull: true acronyme: type: string(10) notnull: true relations: Games: foreignAlias: Platforms class: Game refClass: GamePlatform local: platform_id foreign: game_id GamePlatform: columns: game_id: type: bigint(20) primary: true platform_id: type: bigint(20) primary: true relations: Platform: onDelete: cascade local: platform_id foreign: id foreignAlias: GamePlatforms Game: onDelete: cascade local: game_id foreign: id foreignAlias: GamePlatforms
Avec ce schéma, vous pouvez générer des fichiers et des formulaires qui vont vous faciliter la vie, notamment dans l’insertion de données sans que vous ayez à requêter vous-même.
En savoir plus:
Doctrine 1.2
Symfony with Doctrine saving many-to-many relations