Le chapitre précédent nous a permis d'apprendre à construire des entités. Mais une fois les entités établies, il faut les manipuler !
L'objectif de ce chapitre est donc de voir comment on manipule des entités à l'aide de Doctrine2. Dans un premier temps, nous verrons comment synchroniser les entités avec leur représentation en tables que Doctrine utilise, car en effet, à chaque changement dans une entité, il faut bien que Doctrine mette également à jour la base de données ! Ensuite, nous verrons comment bien manipuler les entités : modification, suppression, etc. Enfin, je vous donnerai un aperçu de la façon de récupérer ses entités depuis la base de données, avant d'aborder cette notion dans un prochain chapitre dédié.
- Matérialiser les tables en base de données
- Enregistrer ses entités avec l'EntityManager
- Récupérer ses entités avec un EntityRepository
Matérialiser les tables en base de données
Avant de pouvoir utiliser notre entité comme il se doit, on doit d'abord créer la table correspondante dans la base de données !
Créer la table correspondante dans la base de données
Alors, j'espère que vous avez installé et configuré phpMyAdmin, on va faire de la requête SQL !
…
Ceux qui m'ont cru, relisez le chapitre précédent. Les autres, venez, on est bien trop fainéants pour ouvrir phpMyAdmin !
Avant toute chose, vérifiez que vous avez bien configuré l'accès à votre base de données dans Symfony2. Si ce n'est pas le cas, il suffit d'ouvrir le fichier app/config/parameters.yml
et de mettre les bonnes valeurs aux lignes commençant par database_
: serveur, nom de la base, nom d'utilisateur et mot de passe. Vous avez l'habitude de ces paramètres, voici les miens, mais adaptez-les à votre cas :
1 2 3 4 5 6 7 8 9 | # app/config/parameters.yml parameters: database_driver: pdo_mysql database_host: localhost database_port: ~ database_name: symfony database_user: root database_password: ~ |
Ensuite, direction la console. Cette fois-ci, on ne va pas utiliser une commande du generator, mais une commande de Doctrine, car on ne veut pas générer du code mais une table dans la base de données.
D'abord, si vous ne l'avez pas déjà fait, il faut créer la base de données. Pour cela, exécutez la commande (vous n'avez à le faire qu'une seule fois évidemment) :
1 2 3 4 | C:\wamp\www\Symfony>php app/console doctrine:database:create Created database for connection named `symfony` C:\wamp\www\Symfony>_ |
Ensuite, il faut générer les tables à l'intérieur de cette base de données. Exécutez donc la commande suivante :
1 | php app/console doctrine:schema:update --dump-sql
|
Cette dernière commande est vraiment performante. Elle va comparer l'état actuel de la base de données avec ce qu'elle devrait être en tenant compte de toutes nos entités. Puis elle affiche les requêtes SQL à exécuter pour passer de l'état actuel au nouvel état.
En l'occurrence, nous avons seulement créé une entité, donc la différence entre l'état actuel (base de données vide) et le nouvel état (base de données avec une table Article
) n'est que d'une seule requête SQL : la requête de création de la table. Doctrine vous affiche donc cette requête :
1 2 3 4 5 6 | CREATE TABLE Article (id INT AUTO_INCREMENT NOT NULL, date DATETIME NOT NULL, titre VARCHAR(255) NOT NULL, auteur VARCHAR(255) NOT NULL, contenu LONGTEXT NOT NULL, PRIMARY KEY(id)) ENGINE = InnoDB; |
Pour l'instant, rien n'a été fait en base de données, Doctrine nous a seulement affiché la ou les requêtes qu'il s'apprête à exécuter. Pensez à toujours valider rapidement ces requêtes, pour être sûrs de ne pas avoir fait d'erreur dans le mapping des entités. Mais maintenant, il est temps de passer aux choses sérieuses, et d'exécuter concrètement cette requête ! Lancez la commande suivante :
1 2 3 4 5 | C:\wamp\www\Symfony>php app/console doctrine:schema:update --force Updating database schema... Database schema updated successfully! "1" queries were executed C:\wamp\www\Symfony>_ |
Si tout se passe bien, vous avez le droit au Database schema updated successfully!
. Génial, mais bon, vérifions-le quand même. Cette fois-ci, ouvrez phpMyAdmin (vraiment, ce n'est pas un piège), allez dans votre base de données et voyez le résultat : la table Article
a bien été créée avec les bonnes colonnes, l'id en auto-incrément, etc. C'est super !
Modifier une entité
Pour modifier une entité, il suffit de lui créer un attribut et de lui attacher l'annotation correspondante. Faisons-le dès maintenant en ajoutant un attribut $publication
, un booléen qui indique si l'article est publié (true
pour l'afficher sur la page d'accueil, false
sinon), ce n'est qu'un exemple bien entendu. Rajoutez donc ces lignes dans votre entité :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php // sdz/Sdz/BlogBundle/Entity/Article.php class Article { // … /** * @ORM\Column(name="publication", type="boolean") */ private $publication; // Et modifions le constructeur pour mettre cet attribut publication à true par défaut public function __construct() { $this->date = new \Datetime(); $this->publication = true; } // … } |
Ensuite, soit vous écrivez vous-mêmes le getter getPublication
et le setter setPublication
, soit vous faites comme moi et vous utilisez le générateur !
Après la commande doctrine:generate:entity
pour générer une entité entière, vous avez la commande doctrine:generate:entities
. C'est une commande qui génère les entités en fonction du mapping que Doctrine connaît. Lorsque vous faites votre mapping en YAML, il peut générer toute votre entité. Dans notre cas, nous faisons notre mapping en annotation, alors nous avons déjà défini l'attribut. La commande va donc générer ce qu'il manque : le getter et le setter !
Allons-y :
1 2 3 4 | C:\wamp\www\Symfony>php app/console doctrine:generate:entities SdzBlogBundle:Article Generating entity "Sdz\BlogBundle\Entity\Article" > backing up Article.php to Article.php~ > generating Sdz\BlogBundle\Entity\Article |
Allez vérifier votre entité, tout en bas de la classe, le générateur a rajouté les méthodes getPublication()
et setPublication()
.
Vous pouvez voir également qu'il a sauvegardé l'ancienne version de votre entité dans un fichier nommé Article.php~
: vérifiez toujours son travail, et si celui-ci ne vous convient pas, vous avez votre sauvegarde.
Maintenant, il ne reste plus qu'à enregistrer ce schéma en base de données. Exécutez donc :
1 | php app/console doctrine:schema:update --dump-sql
|
… pour vérifier que la requête est bien :
1 | ALTER TABLE article ADD publication TINYINT(1) NOT NULL |
C'est le cas, cet outil de Doctrine est vraiment pratique ! Puis exécutez la commande pour modifier effectivement la table correspondante :
1 | php app/console doctrine:schema:update --force
|
Et voilà ! Votre entité a un nouvel attribut qui sera persisté en base de données lorsque vous l'utiliserez.
À retenir
À chaque modification du mapping des entités, ou lors de l'ajout/suppression d'une entité, il faudra répéter ces commandes doctrine:schema:update --dump-sql
puis --force
pour mettre à jour la base de données.
Enregistrer ses entités avec l'EntityManager
Maintenant, apprenons à manipuler nos entités. On va apprendre à le faire en deux parties : d'abord l'enregistrement en base de données, ensuite la récupération depuis la base de données. Mais d'abord, étudions un petit peu le service Doctrine.
Les services Doctrine2
Rappelez-vous, un service est une classe qui remplit une fonction bien précise, accessible partout dans notre code. Dans ce paragraphe, concentrons-nous sur ce qui nous intéresse : accéder aux fonctionnalités Doctrine2 via leurs services.
Le service Doctrine
Le service Doctrine est celui qui va nous permettre de gérer la persistance de nos objets. Ce service est accessible depuis le contrôleur comme n'importe quel service :
1 2 | <?php $doctrine = $this->get('doctrine'); |
Mais, afin de profiter de l'autocomplétion de votre IDE, la classe Controller
de Symfony2 intègre un raccourci. Il fait exactement la même chose, mais est plus joli et permet l'autocomplétion :
1 2 | <?php $doctrine = $this->getDoctrine(); |
C'est donc ce service Doctrine qui va nous permettre de gérer la base de données. Il permet de gérer deux choses :
- Les différentes connexions à des bases de données. C'est la partie DBAL de Doctrine2. En effet, vous pouvez tout à fait utiliser plusieurs connexions à plusieurs bases de données différentes. Cela n'arrive que dans des cas particuliers, mais c'est toujours bon à savoir que Doctrine le gère bien. Le service Doctrine dispose donc, entre autres, de la méthode
$doctrine->getConnection($name)
qui permet de récupérer une connexion à partir de son nom. Cette partie DBAL permet à Doctrine2 de fonctionner sur plusieurs types de SGBDR, tels que MySQL, PostgreSQL, etc. - Les différents gestionnaires d'entités, ou EntityManager. C'est la partie ORM de Doctrine2. Encore une fois, c'est logique, vous pouvez bien sûr utiliser plusieurs gestionnaires d'entités, ne serait-ce qu'un par connexion ! Le service dispose donc, entre autres, de la méthode dont nous nous servirons beaucoup :
$doctrine->getManager($name)
qui permet de récupérer un ORM à partir de son nom.
Dans la suite du tutoriel, je considère que vous n'avez qu'un seul EntityManager, ce qui est le cas par défaut. La méthode getManager()
permet de récupérer l'EntityManager par défaut en omettant l'argument $name
. J'utiliserai donc toujours $doctrine->getManager()
sans argument, mais pensez à adapter si ce n'est pas votre cas !
Si vous souhaitez utiliser plusieurs EntityManager, vous pouvez vous référer à la documentation officielle qui l'explique.
Le service EntityManager
On vient de le voir, le service qui va nous intéresser vraiment n'est pas doctrine, mais l'EntityManager de Doctrine. Vous savez déjà le récupérer depuis le contrôleur via :
1 2 | <?php $em = $this->getDoctrine()->getManager(); |
Pour ceux qui viennent de Symfony2.0, on utilisait avant ->getEntityManager()
, mais depuis la version 2.1 son accès à été simplifié au profit de ->getManager()
.
Mais sachez que, comme tout service qui se respecte, vous pouvez y accéder directement via :
1 2 | <?php $em = $this->get('doctrine.orm.entity_manager'); |
Mais attention, la première méthode vous assure l'autocompletion alors que la deuxième non.
C'est avec l'EntityManager que l'on va passer le plus clair de notre temps. C'est lui qui permet de dire à Doctrine « Persiste cet objet », c'est lui qui va exécuter les requêtes SQL (que l'on ne verra jamais), bref, c'est lui qui fera tout.
La seule chose qu'il ne sait pas faire facilement, c'est récupérer les entités depuis la base de données. Pour faciliter l'accès aux objets, on va utiliser des Repository.
Les repositories
Les repositories sont des objets, qui utilisent un EntityManager en les coulisses, mais qui sont bien plus faciles et pratiques à utiliser de notre point de vue. Je parle des repositories au pluriel car il en existe un par entité. Quand on parle d'un repository en particulier, il faut donc toujours préciser le repository de quelle entité, afin de bien savoir de quoi on parle.
On accède à ces repositories de la manière suivante :
1 2 3 | <?php $em = $this->getDoctrine()->getManager(); $repository_article = $em->getRepository('SdzBlogBundle:Article'); |
L'argument de la méthode getRepository
est l'entité pour laquelle récupérer le repository. Il y a deux manière de spécifier l'entité voulue :
- Soit en utilisant le namespace complet de l'entité. Pour notre exemple, cela donnerait :
'Sdz\BlogBundle\Entity\Article'
. - Soit en utilisant le raccourci
Nom_du_bundle:Nom_de_l'entité
. Pour notre exemple, c'est donc'SdzBlogBundle:Article'
. C'est un raccourci qui fonctionne partout dans Doctrine.
Attention, ce raccourci ne fonctionne que si vous avez mis vos entités dans le namespace Entity
dans votre bundle.
Ce sont donc ces repositories qui nous permettront de récupérer nos entités. Ainsi, pour charger deux entités différentes, il faut d'abord récupérer leur repository respectif. Un simple pli à prendre, mais très logique.
Conclusion
Vous savez maintenant accéder aux principaux acteurs que nous allons utiliser pour manipuler nos entités. Ils reviendront très souvent, sachez les récupérer par cœur, cela vous facilitera la vie. Afin de bien les visualiser, je vous propose à la figure suivante un petit schéma à avoir en tête.
Enregistrer ses entités en base de données
Rappelez-vous, on a déjà vu comment créer une entité. Maintenant que l'on a cette magnifique entité entre les mains, il faut la donner à Doctrine pour qu'il l'enregistre en base de données. L'enregistrement effectif en base de données se fait en deux étapes très simples depuis un contrôleur. Modifiez la méthode ajouterAction()
de notre contrôleur pour faire les tests :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php // Attention à bien ajouter ce use en début de contrôleur use Sdz\BlogBundle\Entity\Article; // … public function ajouterAction() { // Création de l'entité $article = new Article(); $article->setTitre('Mon dernier weekend'); $article->setAuteur('Bibi'); $article->setContenu("C'était vraiment super et on s'est bien amusé."); // On peut ne pas définir ni la date ni la publication, // car ces attributs sont définis automatiquement dans le constructeur // On récupère l'EntityManager $em = $this->getDoctrine()->getManager(); // Étape 1 : On « persiste » l'entité $em->persist($article); // Étape 2 : On « flush » tout ce qui a été persisté avant $em->flush(); // Reste de la méthode qu'on avait déjà écrit if ($this->getRequest()->getMethod() == 'POST') { $this->get('session')->getFlashBag()->add('info', 'Article bien enregistré'); return $this->redirect( $this->generateUrl('sdzblog_voir', array('id' => $article->getId())) ); } return $this->render('SdzBlogBundle:Blog:ajouter.html.twig'); } |
Reprenons ce code :
- La ligne 12 permet de créer l'entité, et les lignes 13 à 15 de renseigner ses attributs ;
- La ligne 20 permet de récupérer l'EntityManager, on en a déjà parlé, je ne reviens pas dessus ;
- L'étape 1 dit à Doctrine de « persister » l'entité. Cela veut dire qu'à partir de maintenant cette entité (qui n'est qu'un simple objet !) est gérée par Doctrine. Cela n'exécute pas encore de requête SQL, ni rien d'autre.
- L'étape 2 dit à Doctrine d'exécuter effectivement les requêtes nécessaires pour sauvegarder les entités qu'on lui a dit de persister ;
- Ligne 31, notre
Article
étant maintenant enregistré en base de données grâce auflush()
, Doctrine2 lui a attribué un id !
Allez sur la page /blog/ajouter
, et voilà, vous venez d'ajouter un article dans la base de données !
Si la requête SQL effectuée vous intéresse, je vous invite à cliquer sur l'icône tout à droite dans la barre d'outil Symfony2 en bas de la page, comme le montre la figure suivante.
Vous arrivez alors dans la partie Doctrine du profiler de Symfony2, et vous pouvez voir les différentes requêtes SQL exécutées par Doctrine. C'est très utile pour vérifier la valeur des paramètres, la structure des requêtes, etc. N'hésitez pas à y faire des tours !
Alors, vous me direz qu'ici on n'a persisté qu'une seule entité, c'est vrai. Mais on peut tout à fait faire plusieurs persists sur différentes entités avant d'exécuter un seul flush. Le flush permet d'exécuter les requêtes les plus optimisées pour enregistrer tous nos persists.
Doctrine utilise les transactions
Pourquoi deux méthodes $em->persist()
et $em->flush()
? Car cela permet entre autres de profiter des transactions. Imaginons que vous ayez plusieurs entités à persister en même temps. Par exemple, lorsque l'on crée un sujet sur un forum, il faut enregistrer l'entité Sujet
, mais aussi l'entité Message
, les deux en même temps. Sans transaction, vous feriez d'abord la première requête, puis la deuxième. Logique au final. Mais imaginez que vous ayez enregistré votre Sujet
, et que l'enregistrement de votre Message
échoue : vous avez un sujet sans message ! Cela casse votre base de données, car la relation n'est plus respectée.
Avec une transaction, les deux entités sont enregistrées en même temps, ce qui fait que si la deuxième échoue, alors la première est annulée, et vous gardez une base de données propre.
Concrètement, avec notre EntityManager, chaque $em->persist()
est équivalent à dire : « Garde cette entité en mémoire, tu l'enregistreras au prochain flush()
. » Et un $em->flush()
est équivalent à : « Ouvre une transaction et enregistre toutes les entités qui t'ont été données depuis le dernier flush()
. »
Doctrine simplifie la vie
Vous devez savoir une chose également : la méthode $em->persist()
traite indifféremment les nouvelles entités de celles déjà en base de données. Vous pouvez donc lui passer une entité fraîchement créée comme dans notre exemple précédent, mais également une entité que vous auriez récupérée grâce à l'EntityRepository et que vous auriez modifiée (ou non, d'ailleurs). L'EntityManager s'occupe de tout, je vous disais !
Concrètement, cela veut dire que vous n'avez plus à vous soucier de faire des INSERT INTO
dans le cas d'une création d'entité, et des UPDATE
dans le cas d'entités déjà existantes. Exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php // Depuis un contrôleur $em = $this->getDoctrine()->getManager(); // On crée un nouvel article $article1 = new Article; $article1->setTitre('Mon dernier weekend'); $article1->setContenu("C'était vraiment super et on s'est bien amusé."); // Et on le persiste $em->persist($article1); // On récupère l'article d'id 5. On n'a pas encore vu cette méthode find(), mais elle est simple à comprendre // Pas de panique, on la voit en détail dans un prochain chapitre dédié aux repositories $article2 = $em->getRepository('SdzBlogBundle:Article')->find(5); // On modifie cet article, en changeant la date à la date d'aujourd'hui $article2->setDate(new \Datetime()); // Ici, pas besoin de faire un persist() sur $article2. En effet, comme on a récupéré cet article via Doctrine, // il sait déjà qu'il doit gérer cette entité. Rappelez-vous, un persist ne sert qu'à donner la responsabilité de l'objet à Doctrine. // Enfin, on applique les changements à la base de données $em->flush(); |
Le flush()
va donc exécuter un INSERT INTO
et un UPDATE
tout seul. De notre côté, on a traité $article1
exactement comme $article2
, ce qui nous simplifie bien la vie. Comment sait-il si l'entité existe déjà ou non ? Grâce à la clé primaire de votre entité (dans notre cas, l'id). Si l'id est nul, c'est une nouvelle entité, tout simplement.
Retenez bien également le fait qu'il est inutile de faire un persist($entite)
lorsque $entite
a été récupérée grâce à Doctrine. En effet, rappelez-vous qu'un persist ne fait rien d'autre que de donner la responsabilité d'un objet à Doctrine. Dans le cas de la variable $article1
de l'exemple précédent, Doctrine ne peut pas deviner qu'il doit s'occuper de cet objet si on ne le lui dit pas ! D'où le persist()
. Mais à l'inverse, comme c'est Doctrine qui nous a donné l'objet $article2
, il est grand et prend tout seul la responsabilité de cet objet, inutile de le lui répéter.
Sachez également que Doctrine est assez intelligent pour savoir si une entité a été modifiée ou non. Ainsi, si dans notre exemple on ne modifiait pas $article2
, Doctrine ne ferait pas de requête UPDATE
inutile.
Les autres méthodes utiles de l'EntityManager
En plus des deux méthodes les plus importantes, persist()
et flush()
, l'EntityManager dispose de quelques méthodes intéressantes. Je ne vais vous présenter ici que les plus utilisées, mais elles sont bien sûr toutes documentées dans la documentation officielle, que je vous invite fortement à aller voir.
clear($nomEntite)
annule tous les persist()
effectués. Si le nom d'une entité est précisé (son namespace complet ou son raccourci), seuls les persist()
sur des entités de ce type seront annulés. Voici un exemple :
1 2 3 4 5 | <?php $em->persist($article); $em->persist($commentaire); $em->clear(); $em->flush(); // N'exécutera rien, car les deux persists sont annulés par le clear |
detach($entite)
annule le persist()
effectué sur l'entité en argument. Au prochain flush()
, aucun changement ne sera donc appliqué à l'entité. Voici un exemple :
1 2 3 4 5 | <?php $em->persist($article); $em->persist($commentaire); $em->detach($article); $em->flush(); // Enregistre $commentaire mais pas $article |
contains($entite)
retourne true
si l'entité donnée en argument est gérée par l'EntityManager (s'il y a eu un persist()
sur l'entité donc). Voici un exemple :
1 2 3 4 | <?php $em->persist($article); var_dump($em->contains($article)); // Affiche true var_dump($em->contains($commentaire)); // Affiche false |
refresh($entite)
met à jour l'entité donnée en argument dans l'état où elle est en base de données. Cela écrase et donc annule tous les changements qu'il a pu y avoir sur l'entité concernée. Voici un exemple :
1 2 3 4 | <?php $article->setTitre('Un nouveau titre'); $em->refresh($article); var_dump($article->getTitre()); // Affiche « Un ancien titre » |
remove($entite)
supprime l'entité donnée en argument de la base de données. Effectif au prochain flush()
. Voici un exemple :
1 2 3 | <?php $em->remove($article); $em->flush(); // Exécute un DELETE sur $article |
Récupérer ses entités avec un EntityRepository
Un prochaine chapitre entier est consacré aux repositories, juste après dans cette partie sur Doctrine. Les repositories ne sont qu'un outil pour récupérer vos entités très facilement, nous apprendrons à les maîtriser entièrement. Mais en avant première, sachez au moins récupérer une unique entité en fonction de son id.
Il faut d'abord pour cela récupérer le repository de l'entité que vous voulez. On l'a vu précédemment, voici un rappel :
1 2 3 4 5 6 | <?php // Depuis un contrôleur $repository = $this->getDoctrine() ->getManager() ->getRepository('SdzBlogBundle:Article'); |
Puis depuis ce repository, il faut utiliser la méthode find($id)
qui permet de retourner l'entité correspondant à l'id $id
. Je vous invite à essayer ce code directement dans la méthode voirAction()
de notre contrôleur Blog, là où on avait défini en dur un tableau $article
. On pourra ainsi voir l'effet immédiatement :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php // … public function voirAction($id) { // On récupère le repository $repository = $this->getDoctrine() ->getManager() ->getRepository('SdzBlogBundle:Article'); // On récupère l'entité correspondant à l'id $id $article = $repository->find($id); // $article est donc une instance de Sdz\BlogBundle\Entity\Article // Ou null si aucun article n'a été trouvé avec l'id $id if($article === null) { throw $this->createNotFoundException('Article[id='.$id.'] inexistant.'); } return $this->render('SdzBlogBundle:Blog:voir.html.twig', array( 'article' => $article )); } |
Allez voir le résultat sur la page /blog/article/1
! Vous pouvez changer l'id de l'article à récupérer dans l'URL, en fonction des articles que vous avez ajoutés plus haut depuis la méthode ajouterAction()
.
Sachez aussi qu'il existe une autre syntaxe pour faire la même chose directement depuis l'EntityManager, je vous la présente afin que vous ne soyez pas surpris si vous la croisez :
1 2 3 4 5 6 7 8 | <?php // Depuis un contrôleur $article = $this->getDoctrine() ->getManager() ->find('SdzBlogBundle:Article', $id); // 1er argument : le nom de l'entité // 2e argument : l'id de l'instance à récupérer |
Je n'en dis pas plus pour le moment, patientez jusqu'au chapitre consacré aux repositories !
En résumé
- Il faut exécuter la commande
doctrine:schema:update
pour mettre à jour la base de données et la faire correspondre à l'état actuel de vos entités ; - Avec Symfony2, on récupère l'EntityManager de Doctrine2 via le service
doctrine.orm.entity_manager
ou, plus simplement depuis un contrôleur, via$this->getDoctrine()->getManager()
; - L'EntityManager sert à manipuler les entités, tandis que les repositories servent à récupérer les entités.