• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/13/19

Manipuler ses entités avec Doctrine2

Log in or subscribe for free to enjoy all this course has to offer!

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

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 Symfony. Si ce n'est pas le cas, il suffit d'ouvrir le fichierapp/config/parameters.ymlet de mettre les bonnes valeurs aux lignes commençant pardatabase_: 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 :

# 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) :

C:\wamp\www\Symfony>php bin/console doctrine:database:create
Created database `symfony` for connection named default

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 :

php bin/console doctrine:schema:update --dump-sql

Cette dernière commande est vraiment performante. Elle compare 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 créé seulement 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 tableAdvert) 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 :

CREATE TABLE Advert (
    id INT AUTO_INCREMENT NOT NULL,
    date DATETIME NOT NULL,
    title VARCHAR(255) NOT NULL,
    author VARCHAR(255) NOT NULL,
    content 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 :

C:\wamp\www\Symfony>php bin/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "1" query were executed

C:\wamp\www\Symfony>_

Si tout se passe bien, vous avez le droit auDatabase 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 tableAdvert a bien été créée avec les bonnes colonnes, l'id en auto-incrément, etc. C'est super !

Visualisation de la table dans PhpMyAdmin
Visualisation de la table dans PhpMyAdmin

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$published, un booléen qui indique si l'annonce est publiée (truepour l'afficher sur la page d'accueil,falsesinon), ce n'est qu'un exemple bien entendu. Rajoutez donc ces lignes (18 à 21) dans votre entité :

<?php
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Advert
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\AdvertRepository")
 */
class Advert
{
  // ... les autres attributs

  /**
   * @ORM\Column(name="published", type="boolean")
   */
  private $published = true;
  
  // ...
}

Ensuite, soit vous écrivez vous-mêmes le gettergetPublished et le settersetPublished, soit vous faites comme moi et vous utilisez le générateur !

Après la commandedoctrine:generate:entitypour générer une entité entière, vous avez la commandedoctrine: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 et son annotation. La commande va donc générer ce qu'il manque : le getter et le setter !

Allons-y :

C:\wamp\www\Symfony>php bin/console doctrine:generate:entities OCPlatformBundle:Advert
Generating entity "OC\PlatformBundle\Entity\Advert"
  > backing up Advert.php to Advert.php~
  > generating OC\PlatformBundle\Entity\Advert

Allez vérifier votre entité, tout en bas de la classe, le générateur a rajouté les méthodesgetPublished()etsetPublished().

Maintenant, il ne reste plus qu'à enregistrer ce schéma en base de données. Exécutez donc :

php bin/console doctrine:schema:update --dump-sql

… pour vérifier que la requête est bien :

ALTER TABLE advert ADD published 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 :

php bin/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 commandesdoctrine:schema:update --dump-sqlpuis--forcepour 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 gère la persistance de nos objets. Il est accessible depuis le contrôleur comme n'importe quel service :

<?php
$doctrine = $this->get('doctrine');

La classeControllerde Symfony intègre un raccourci. Il fait exactement la même chose, mais est plus joli et permet l'autocomplétion :

<?php
$doctrine = $this->getDoctrine();

C'est donc ce service Doctrine qui gère la base de données. Il s'occupe de 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 récupère 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 récupère un ORM à partir de son nom.

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 :

<?php
$em = $this->getDoctrine()->getManager();

Mais sachez que, comme tout service qui se respecte, vous pouvez y accéder directement via :

<?php
$em = $this->get('doctrine.orm.entity_manager');

Mais attention, la première méthode vous assure l'autocomplétion, c'est celle que nous utiliserons, alors que la deuxième pas forcément (cela dépend en fait de votre IDE) ;).

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 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 :

<?php
$em = $this->getDoctrine()->getManager();
$advertRepository = $em->getRepository('OCPlatformBundle:Advert');

L'argument de la méthodegetRepositoryest l'entité pour laquelle récupérer le repository. Il y a deux manières de spécifier l'entité voulue :

  • Soit en utilisant le namespace complet de l'entité. Pour notre exemple, cela donnerait :'OC\PlatformBundle\Entity\Advert'.

  • Soit en utilisant le raccourciNom_du_bundle:Nom_de_l'entité. Pour notre exemple, c'est donc'OCPlatformBundle:Advert'. C'est un raccourci qui fonctionne partout dans Doctrine.

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 sur la figure suivante, un petit schéma à avoir en tête.

Schéma de l'organisation de Doctrine2
Schéma de l'organisation de Doctrine2

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éthodeaddAction()de notre contrôleur pour faire les tests :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use OC\PlatformBundle\Entity\Advert;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class AdvertController extends Controller
{
  public function addAction(Request $request)
  {
    // Création de l'entité
    $advert = new Advert();
    $advert->setTitle('Recherche développeur Symfony.');
    $advert->setAuthor('Alexandre');
    $advert->setContent("Nous recherchons un développeur Symfony débutant sur Lyon. Blabla…");
    // 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($advert);

    // É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 ($request->isMethod('POST')) {
      $request->getSession()->getFlashBag()->add('notice', 'Annonce bien enregistrée.');

      // Puis on redirige vers la page de visualisation de cettte annonce
      return $this->redirectToRoute('oc_platform_view', array('id' => $advert->getId()));
    }

    // Si on n'est pas en POST, alors on affiche le formulaire
    return $this->render('OCPlatformBundle:Advert:add.html.twig', array('advert' => $advert));
  }
}

Reprenons ce code :

  • La ligne 15 permet de créer l'entité, et les lignes 16 à 18 de renseigner ses attributs ;

  • La ligne 23 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 précédemment (il fait donc desINSERT INTO  & Cie) ;

  • Ligne 36, notreAdvert étant maintenant enregistré en base de données grâce auflush(), Doctrine2 lui a attribué un id ! On peut donc utiliser$advert->getId()  dans la génération de la route, et non un nombre fixe comme précédemment.

Allez sur la page /platform/add, et voilà, vous venez d'ajouter une annonce 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 Symfony en bas de la page, comme le montre la figure suivante.

Ma page a exécuté 3 requêtes en l'occurrence
Ma page a exécuté 3 requêtes en l'occurrence

Vous arrivez alors dans la partie Doctrine du profiler de Symfony, 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 !

On voit les requêtes effectuées
On voit les requêtes effectuées

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é votreSujet, et que l'enregistrement de votreMessageé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 prochainflush(). » Et un$em->flush()est équivalent à : « Ouvre une transaction et enregistre toutes les entités qui t'ont été données depuis le dernierflush(). »

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 au repository 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 desINSERT INTOdans le cas d'une création d'entité, et desUPDATEdans le cas d'entités déjà existantes. Exemple :

<?php
// Depuis un contrôleur

$em = $this->getDoctrine()->getManager();

// On crée une nouvelle annonce
$advert1 = new Advert;
$advert1->setTitle('Recherche développeur.');
$advert1->setContent("Pour mission courte");
// Et on le persiste
$em->persist($advert1);

// On récupère l'annonce 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
$advert2 = $em->getRepository('OCPlatformBundle:Advert')->find(5);

// On modifie cette annonce, en changeant la date à la date d'aujourd'hui
$advert2->setDate(new \Datetime());

// Ici, pas besoin de faire un persist() sur $advert2. En effet, comme on a
// récupéré cette annonce 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 deux changements à la base de données :
// Un INSERT INTO pour ajouter $advert1
// Et un UPDATE pour mettre à jour la date de $advert2
$em->flush();

Leflush()va donc exécuter unINSERT INTOet unUPDATEtout seul. De notre côté, on a traité$advert1 exactement comme$advert2, 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 qu'il est inutile de faire unpersist($entity)lorsque$entity a été récupérée grâce à Doctrine. En effet, rappelez-vous qu'un persist ne fait rien d'autre que donner la responsabilité d'un objet à Doctrine. Dans le cas de la variable$advert1 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ù lepersist(). Mais à l'inverse, comme c'est Doctrine qui nous a donné l'objet$advert2, 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$advert2, Doctrine ne ferait pas de requêteUPDATEinutile.

Les autres méthodes utiles de l'EntityManager

En plus des deux méthodes les plus importantes,persist()etflush(), 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.

detach($entite)  annule lepersist() effectué sur l'entité en argument. Au prochainflush(), aucun changement ne sera donc appliqué à l'entité. Voici un exemple :

<?php
$em->persist($advert);
$em->persist($comment);
$em->detach($advert);
$em->flush(); // Enregistre $comment mais pas $advert

clear($nomEntite) annule tous lespersist()effectués. Si le nom d'une entité est précisé (son namespace complet ou son raccourci), seuls lespersist()sur des entités de ce type seront annulés. Siclear() est appelé sans argument, cela revient à faire undetach() sur toutes les entités d'un coup. Voici un exemple :

<?php
$em->persist($advert);
$em->persist($comment);
$em->clear();
$em->flush(); // N'exécutera rien, car les deux persists sont annulés par le clear

 

contains($entite)retournetruesi l'entité donnée en argument est gérée par l'EntityManager (s'il y a eu unpersist()sur l'entité donc). Voici un exemple :

<?php
$em->persist($advert);
var_dump($em->contains($advert)); // Affiche true
var_dump($em->contains($comment)); // 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 :

<?php
$advert->setTitle('Un nouveau titre');
$em->refresh($advert);
var_dump($advert->getTitle()); // Affiche « Un ancien titre »

remove($entite)supprime l'entité donnée en argument de la base de données. Effectif au prochainflush(). Voici un exemple :

<?php
$em->remove($advert);
$em->flush(); // Exécute un DELETE sur $advert

Récupérer ses entités avec un Repository

Un prochain 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 :

<?php
// Depuis un contrôleur

$repository = $this->getDoctrine()
  ->getManager()
  ->getRepository('OCPlatformBundle:Advert')
;

Puis, depuis ce repository, il faut utiliser la méthodefind($id)qui retourne l'entité correspondant à l'id$id. Je vous invite à essayer ce code directement dans la méthodeviewAction()de notre contrôleur Advert, là où on avait défini en dur un tableau$advert. On pourra ainsi voir l'effet immédiatement :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class AdvertController extends Controller
{
  public function viewAction($id)
  {
    // On récupère le repository
    $repository = $this->getDoctrine()
      ->getManager()
      ->getRepository('OCPlatformBundle:Advert')
    ;

    // On récupère l'entité correspondante à l'id $id
    $advert = $repository->find($id);

    // $advert est donc une instance de OC\PlatformBundle\Entity\Advert
    // ou null si l'id $id  n'existe pas, d'où ce if :
    if (null === $advert) {
      throw new NotFoundHttpException("L'annonce d'id ".$id." n'existe pas.");
    }

    // Le render ne change pas, on passait avant un tableau, maintenant un objet
    return $this->render('OCPlatformBundle:Advert:view.html.twig', array(
      'advert' => $advert
    ));
  }
}

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. Il s'agit de la méthode find de l'EntityManager, et non du Repository :

<?php

// Depuis un contrôleur

$advert = $this->getDoctrine()
  ->getManager()
  ->find('OCPlatformBundle:Advert', $id)
;

Son premier argument est le nom de l'entité. J'ai utilisé ici le raccourci mais vous pouvez utiliser le namespace complet bien entendu. Son deuxième argument est 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 commandedoctrine:schema:updatepour mettre à jour la base de données et la faire correspondre à l'état actuel de vos entités.

  • Avec Symfony, on récupère l'EntityManager de Doctrine2 via le servicedoctrine.orm.entity_managerou, 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.

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-9 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement