• 30 heures
  • Facile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 04/09/2017

Les évènements et extensions Doctrine

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Maintenant que vous savez manipuler vos entités, vous allez vous rendre compte que pas mal de comportements sont répétitifs. En bon développeurs, il est hors de question de dupliquer du code ou de perdre du temps : nous sommes bien trop fainéants !

Ce chapitre a pour objectif de vous présenter les évènements et les extensions Doctrine, qui vous permettront de simplifier certains cas usuels que vous rencontrerez.

Les évènements Doctrine

L'intérêt des évènements Doctrine

Dans certains cas, vous pouvez avoir besoin d'effectuer des actions juste avant ou juste après la création, la mise à jour ou la suppression d'une entité. Par exemple, si vous stockez la date d'édition d'une annonce, à chaque modification de l'entitéAdvert il faut mettre à jour cet attribut juste avant la mise à jour dans la base de données.

Ces actions, vous devez les faire à chaque fois. Cet aspect systématique a deux impacts. D'une part, cela veut dire qu'il faut être sûrs de vraiment les effectuer à chaque fois pour que votre base de données soit cohérente. D'autre part, cela veut dire qu'on est bien trop fainéants pour se répéter !

C'est ici qu'interviennent les évènements Doctrine. Plus précisément, vous les trouverez sous le nom de callbacks du cycle de vie (lifecycle en anglais) d'une entité. Un callback est une méthode de votre entité, et on va dire à Doctrine de l'exécuter à certains moments.

On parle d'évènements de « cycle de vie », car ce sont différents évènements que Doctrine déclenche à chaque moment de la vie d'une entité : son chargement depuis la base de données, sa modification, sa suppression, etc. On en reparle plus loin, je vous dresserai une liste complète des évènements et de leur utilisation.

Définir des callbacks de cycle de vie

Pour vous expliquer le principe, nous allons prendre l'exemple de notre entitéAdvert, qui va comprendre un attribut$updatedAt représentant la date de la dernière édition de l'annonce. Si vous ne l'avez pas déjà, ajoutez-le maintenant, et n'oubliez pas de mettre à jour la base de données à l'aide de la commandedoctrine:schema:update :

<?php
/**
 * @ORM\Column(name="updated_at", type="datetime", nullable=true)
 */
private $updatedAt;
1. Définir l'entité comme contenant des callbacks

Tout d'abord, on doit dire à Doctrine que notre entité contient des callbacks de cycle de vie ; cela se définit grâce à l'annotationHasLifecycleCallbacksdans le namespace habituel des annotations Doctrine :

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

namespace OC\PlatformBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\AdvertRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Advert
{
  // …
}

Cette annotation permet à Doctrine de vérifier les callbacks éventuels contenus dans l'entité. Elle s'applique à la classe de l'entité, et non à un attribut particulier. Ne l'oubliez pas, car sinon vos différents callbacks seront tout simplement ignorés.

2. Définir un callback et ses évènements associés

Maintenant, il faut définir des méthodes et surtout, les évènements sur lesquels elles seront exécutées.

Continuons dans notre exemple, et créons une méthodeupdateDate()dans l'entitéAdvert. Cette méthode doit définir l'attribut$updatedAt à la date actuelle, afin de mettre à jour automatiquement la date d'édition d'une annonce. Voici à quoi elle pourrait ressembler :

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

namespace OC\PlatformBundle\Entity;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\AdvertRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Advert
{
  // …

  public function updateDate()
  {
    $this->setUpdatedAt(new \Datetime());
  }
}

Maintenant il faut dire à Doctrine d'exécuter cette méthode (ce callback) dès que l'entitéAdvert est modifiée. On parle d'écouter un évènement. Il existe plusieurs évènements de cycle de vie avec Doctrine, celui qui nous intéresse ici est l'évènementPreUpdate: c'est-à-dire que la méthode va être exécutée juste avant que l'entité ne soit modifiée en base de données. Voici à quoi cela ressemble :

<?php

/**
 * @ORM\PreUpdate
 */
public function updateDate()

C'est tout !

Vous pouvez dès à présent tester le comportement. Essayez de faire un petit code de test pour charger une annonce, la modifier, et l'enregistrer (avec unflush()), vous verrez que l'attribut$updatedAt va se mettre à jour automatiquement. Attention, l'évènementupdaten'est pas déclenché à la création d'une entité, mais seulement à sa modification : c'est parfaitement ce qu'on veut dans notre exemple.

Pour aller plus loin, il y a deux points qu'il vous faut savoir. D'une part, au même titre que l'évènementPreUpdate, il existe l'évènementPostUpdateet bien d'autres, on en dresse une liste dans le tableau suivant. D'autre part, vous l'avez sûrement noté, mais le callback ne prend aucun argument, vous ne pouvez en effet utiliser et modifier que l'entité courante. Pour exécuter des actions plus complexes lors d'évènements, il faut créer des services, on voit cela plus loin.

Liste des évènements de cycle de vie

Les différents évènements du cycle de vie sont récapitulés dans le tableau suivant.

Évènement 

  Description

PrePersist

L'évènement PrePersist se produit juste avant que l'EntityManager ne persiste effectivement l'entité. Concrètement, cela exécute le callback juste avant un $em->persist($entity). Il ne concerne que les entités nouvellement créées. Du coup, il y a deux conséquences : d'une part, les modifications que vous apportez à l'entité seront persistées en base de données, puisqu'elles sont effectives avant que l'EntityManager n'enregistre l'entité en base. D'autre part, vous n'avez pas accès à l'id de l'entité si celui-ci est autogénéré, car justement l'entité n'est pas encore enregistrée en base de données, et donc l'id pas encore généré.

PostPersist

L'évènement postPersist se produit juste après que l'EntityManager ait effectivement persisté l'entité. Attention, cela n'exécute pas le callback juste après le $em->persist($entity), mais juste après le $em->flush(). À l'inverse du prePersist, les modifications que vous apportez à l'entité ne seront pas persistées en base (mais seront tout de même appliquées à l'entité, attention) ; mais vous avez par contre accès à l'id qui a été généré lors du flush().

PreUpdate

L'évènement preUpdate se produit juste avant que l'EntityManager ne modifie une entité. Par modifiée, j'entends que l'entité existait déjà, que vous y avez apporté des modifications, puis un $em->flush(). Le callback sera exécuté juste avant le flush(). Attention, il faut que vous ayez modifié au moins un attribut pour que l'EntityManager génère une requête et donc déclenche cet évènement.
Vous avez accès à l'id autogénéré (car l'entité existe déjà), et vos modifications seront persistées en base de données.

PostUpdate

L'évènement postUpdate se produit juste après que l'EntityManager a effectivement modifié une entité. Vous avez accès à l'id et vos modifications ne sont pas persistées en base de données.

PreRemove

L'évènement PreRemove se produit juste avant que l'EntityManager ne supprime une entité, c'est-à-dire juste avant un $em->flush() qui précède un $em->remove($entite). Attention, soyez prudents dans cet évènement, si vous souhaitez supprimer des fichiers liés à l'entité par exemple, car à ce moment l'entité n'est pas encore effectivement supprimée, et la suppression peut être annulée en cas d'erreur dans une des opérations à effectuer dans le flush().

PostRemove

L'évènement PostRemove se produit juste après que l'EntityManager a effectivement supprimé une entité. Si vous n'avez plus accès à son id, c'est ici que vous pouvez effectuer une suppression de fichier associé par exemple.

PostLoad

L'évènement PostLoad se produit juste après que l'EntityManager a chargé une entité (ou après un $em->refresh()). Utile pour appliquer une action lors du chargement d'une entité.

 

Un autre exemple d'utilisation

Pour bien comprendre l'intérêt des évènements, je vous propose un deuxième exemple : un compteur de candidatures pour les annonces.

L'idée est la suivante : nous avons un site très fréquenté, et un petit serveur. Au lieu de récupérer le nombre de candidatures par annonce à l'aide d'une requêteCOUNT(*), on décide de rajouter un attributnbApplications à notre entitéAdvert. L'enjeu maintenant est de tenir cet attribut parfaitement à jour, et surtout très facilement.

C'est là que les évènements interviennent. Si on réfléchit un peu, le processus est assez simple et systématique :

  • À chaque création d'une candidature, on doit effectuer un+1au compteur contenu dans l'entitéAdvert liée ;

  • À chaque suppression d'une candidature, on doit effectuer un-1au compteur contenu dans l'entité l'Advert liée.

Ce genre de comportement, relativement simple et systématique, est typiquement ce que nous pouvons automatiser grâce aux évènements Doctrine.

Les deux évènements qui nous intéressent ici sont donc la création et la suppression d'une candidature. Il s'agit des évènementsPrePersistetPreRemove de l'entitéApplication. Pourquoi ? Car les évènements*Updatesont déclenchés à la mise à jour d'une candidature, ce qui ne change pas notre compteur ici. Et les évènementsPost*sont déclenchés après la mise à jour effective de l'entité dans la base de données, du coup la mise à jour de notre compteur ne serait pas enregistrée.

Tout d'abord, créons notre attribut$nbApplications dans l'entitéAdvert, ainsi que des méthodes pour incrémenter et décrémenter ce compteur :

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

namespace OC\PlatformBundle\Entity;

class Advert
{
  /**
   * @ORM\Column(name="nb_applications", type="integer")
   */
  private $nbApplications = 0;

  public function increaseApplication()
  {
    $this->nbApplications++;
  }

  public function decreaseApplication()
  {
    $this->nbApplications--;
  }
  
  // ...
}

Ensuite, on doit définir deux callbacks dans l'entitéApplication pour mettre à jour le compteur de l'entitéAdvert liée. Voici comment on pourrait faire :

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

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Entity\ApplicationRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Application
{
  /**
   * @ORM\PrePersist
   */
  public function increase()
  {
    $this->getAdvert()->increaseApplication();
  }

  /**
   * @ORM\PreRemove
   */
  public function decrease()
  {
    $this->getAdvert()->decreaseApplication();
  }

  // ...
}

Cette solution est possible car nous avons une relation entre ces deux entitésApplication etAdvert, il est donc possible d'accéder à l'annonce depuis une candidature.

Utiliser des services pour écouter les évènements Doctrine

Les callbacks définis directement dans les entités sont pratiques car très simple à mettre en place. Quelques petites annotations et le tour est joué. Cependant, leurs limites sont vite atteintes car, comme toute méthode au sein d'une entité, les callbacks n'ont accès à aucune information de l'extérieur.

En effet, imaginez qu'on veuille mettre en place un système pour envoyer un email à chaque création d'une candidature. Dans ce cas, le code qui est exécuté à chaque création d'entité a besoin du service mailer afin d'envoyer des emails, or ce n'est pas possible depuis une entité.

Heureusement, il est possible de dire à Doctrine d'exécuter de simples services pour chaque évènement du cycle de vie des entités. L'idée est vraiment la même, au lieu d'une méthode callback dans notre entité, on a un service défini hors de notre entité. La seule différence est la syntaxe bien sûr.

Il y a tout de même un point qui diffère des callbacks, c'est que nos services seront exécutés pour un évènement (PostPersist par exemple) concernant toutes nos entités, et non attaché à une seule entité. Si vous voulez effectuer votre action seulement pour les entitésAdvert, il faut alors vérifier le type d'entité qui sera en argument. L'avantage est que du coup vous pouvez facliement effectuer une action commune à toutes vos entités.

La syntaxe à respecter est relativement simple, je vais vous donner un exemple vous allez comprendre assez vite. Tout d'abord, voici l'objet que je vous propose pour envoyer les emails :

<?php
// src/OC/PlatformBundle/DoctrineListener/ApplicationNotification.php

namespace OC\PlatformBundle\DoctrineListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use OC\PlatformBundle\Entity\Application;

class ApplicationNotification
{
  private $mailer;

  public function __construct(\Swift_Mailer $mailer)
  {
    $this->mailer = $mailer;
  }

  public function postPersist(LifecycleEventArgs $args)
  {
    $entity = $args->getEntity();

    // On veut envoyer un email que pour les entités Application
    if (!$entity instanceof Application) {
      return;
    }

    $message = new \Swift_Message(
      'Nouvelle candidature',
      'Vous avez reçu une nouvelle candidature.'
    );
    
    $message
      ->addTo($entity->getAdvert()->getAuthor()) // Ici bien sûr il faudrait un attribut "email", j'utilise "author" à la place
      ->addFrom('admin@votresite.com')
    ;

    $this->mailer->send($message);
  }
}

Notez que j'ai nommé la méthode du service du nom de l'évènement que nous allons écouter. Nous ferons effectivement le lien avec l'évènement via la configuration du service, mais la méthode doit respecter le même nom que l'évènement.

Ensuite, il y a deux points à retenir sur la syntaxe :

  • Le seul argument qui est donné à votre méthode est un objet LifecycleEventArgs. Il offre deux méthodes : getEntity et getEntityManager. La première, getEntity, retourne l'entité sur laquelle l'évènement est en train de se produire. La seconde, getEntityManager, retourne l'EntityManager nécessaire pour persister ou supprimer de nouvelles entités que vous pourriez gérer, nous ne nous en servons pas ici ;

  • Comme je l'avais mentionné, la méthode sera exécutée pour l'évènement PostPersist de toutes vos entités. Dans notre cas, comme souvent, nous ne voulons envoyer l'email que lorsqu'une entité en particulier est ajoutée, iciApplication. D'où le if pour vérifier le type d'entité auquel on a affaire.

Maintenant que notre objet est prêt, il faut en faire un service et dire à Doctrine qu'il doit être exécuté pour tous les évènements PostPersist. Voici la syntaxe à respecter :
# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.doctrine.notification:
        class: OC\PlatformBundle\DoctrineListener\ApplicationNotification
        arguments: [@mailer]
        tags:
            - { name: doctrine.event_listener, event: postPersist }

La définition du service n'a rien de nouveau par rapport à ce que nous avons vu sur le chapitre dédié aux services.

La nouveauté est par contre la section tag. Sachez simplement que ce tag permet au conteneur de services de dire à Doctrine que ce service doit être exécuté pour les évènements PostPersist. Pas de panique, je vous explique en détail le fonctionnement des tags de services dans un chapitre de la prochaine partie, mais pour l'instant connaître cette syntaxe vous permet de les utiliser dans ce cadre.

Bien entendu, vous pouvez écouter n'importe quel évènement avec cette syntaxe, il vous suffit de modifier l'attribut "event: PostPersist" du tag.

Les extensions Doctrine

L'intérêt des extensions Doctrine

Dans la gestion des entités d'un projet, il y a des comportements assez communs que vous souhaiterez implémenter.

Par exemple, il est très classique de vouloir générer des slugs pour nos annonces, pour des sujets d'un forum, etc. Un slug est une version simplifiée, compatible avec les URL, d'un autre attribut, souvent un titre. Par exemple le slug du titre "Recherche développeur !" serait "recherche-developpeur", notez que l'espace a été remplacé par un tiret, le point d'exclamation supprimé.

Plutôt que de réinventer tout le comportement nous-mêmes, nous allons utiliser les extensions Doctrine ! Doctrine2 est en effet très flexible, et la communauté a déjà créé une série d'extensions Doctrine très pratiques afin de vous aider avec les tâches usuelles liées aux entités. À l'image des évènements, utiliser ces extensions évite de se répéter au sein de votre application Symfony2 : c'est la philosophie DRY.

Installer le StofDoctrineExtensionBundle

Un bundle en particulier permet d'intégrer différentes extensions Doctrine dans un projet Symfony2, il s'agit de StofDoctrineExtensionsBundle. Commençons par l'installer avec Composer, rajoutez cette dépendance dans votrecomposer.json:

// composer.json

"require": {
  "stof/doctrine-extensions-bundle": "~1.1"
}

Ce bundle intègre la bibliothèque DoctrineExtensions sous-jacente, qui est celle qui inclut réellement les extensions Doctrine.

N'oubliez pas d'enregistrer le bundle dans le noyau :

<?php
// app/AppKernel.php

public function registerBundles()
{
  return array(
    // …
    new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
    // …
  );
}

Voilà le bundle est installé, il faut maintenant activer telle ou telle extension.

Utiliser une extension : l'exemple de Sluggable

L'utilisation des différentes extensions est très simple grâce à la flexibilité de Doctrine2 et au bundle pour Symfony2. Voici par exemple l'utilisation de l'extension Sluggable, qui permet de définir très facilement un attributslugdans une entité : le slug sera automatiquement généré !

Tout d'abord, il faut activer l'extension Sluggable, il faut pour cela configurer le bundle via le fichier de configurationconfig.yml. Rajoutez donc cette section :

# app/config/config.yml

# Stof\DoctrineExtensionsBundle configuration
stof_doctrine_extensions:
    orm:
        default:
            sluggable: true

Cela va activer l'extension Sluggable. De la même manière, vous pourrez activer les autres extensions en les rajoutant à la suite.

Concrètement, l'utilisation des extensions se fait grâce à de judicieuses annotations. Vous l'aurez deviné, pour l'extension Sluggable, l'annotation est tout simplementSlug. En l'occurrence, il faut ajouter un nouvel attributslug(le nom est arbitraire) dans votre entité, sur lequel nous mettrons l'annotation. Voici un exemple dans notre entitéAdvert :

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

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
// N'oubliez pas ce use :
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity
 */
class Advert
{
  // …

  /**
   * @Gedmo\Slug(fields={"title"})
   * @ORM\Column(length=128, unique=true)
   */
  private $slug;

  // …
}

Dans un premier temps, vous avez l'habitude, on utilise le namespace de l'annotation, iciGedmo\Mapping\Annotation.

Ensuite, l'annotation Slug s'applique très simplement sur un attribut qui va contenir le slug. L'optionfieldspermet de définir le ou les attributs à partir desquels le slug sera généré : ici le titre uniquement. Mais vous pouvez en indiquer plusieurs en les séparant par des virgules.

C'est tout ! Vous pouvez dès à présent tester le nouveau comportement de votre entité. Créez une entité avec un titre de test, et enregistrez-la : son attributslugsera automatiquement rempli ! Par exemple :

<?php
// Dans un contrôleur

public function testAction()
{
  $advert = new Advert();
  $advert->setTitle("Recherche développeur !");
  $advert->setAuthor('Marine');
  $advert->setContent("Nous recherchons un développeur Symfony débutant sur Lyon. Blabla…");

  $em = $this->getDoctrine()->getManager();
  $em->persist($advert);
  $em->flush(); // C'est à ce moment qu'est généré le slug

  return new Response('Slug généré : '.$advert->getSlug());
  // Affiche « Slug généré : recherche-developpeur »
}

L'attributslugest rempli automatiquement par le bundle. Ce dernier utilise en réalité tout simplement les évènements DoctrinePrePersistetPreUpdate, qui permettent d'intervenir juste avant l'enregistrement et la modification de l'entité comme on l'a vu plus haut.

Vous savez maintenant utiliser l'extension Doctrine Sluggable ! Voyons les autres extensions disponibles.

Liste des extensions Doctrine

Voici la liste des principales extensions actuellement disponibles, ainsi que leur description et des liens vers la documentation pour vous permettre de les implémenter dans votre projet.

Extension

Description

Tree

L'extension Tree automatise la gestion des arbres et ajoute des méthodes spécifiques au repository. Les arbres sont une représentation d'entités avec des liens type parents-enfants, utiles pour les catégories d'un forum par exemple.

Translatable

L'extension Translatable offre une solution aisée pour traduire des attributs spécifiques de vos entités dans différents langages. De plus, elle charge automatiquement les traductions pour la locale courante.

Sluggable

L'extension Sluggable permet de générer automatiquement un slug à partir d'attributs spécifiés.

Timestampable

L'extension Timestampable automatise la mise à jour d'attributs de typedatedans vos entités. Vous pouvez définir la mise à jour d'un attribut à la création et/ou à la modification, ou même à la modification d'un attribut particulier. Vous l'aurez compris, cette extension fait exactement la même chose que ce qu'on a fait dans le paragraphe précédent sur les évènements Doctrine (mise à jour de la date à chaque modification), et en mieux !

Blameable

L'extension Blameable permet d'assigner l'utilisateur courant (l'entité elle-même, ou alors juste le nom d'utilisateur) dans un attribut d'une autre entité. Utile pour notre entitéAdvert par exemple, laquelle pourrait être reliée à un utilisateur.

Loggable

L'extension Loggable permet de conserver les différentes versions de vos entités, et offre des outils de gestion des versions.

Sortable

L'extension Sortable permet de gérer des entités ordonnées, c'est-à-dire avec un ordre précis.

Softdeleteable

L'extension SoftDeleteable permet de « soft-supprimer » des entités, c'est-à-dire de ne pas les supprimer réellement, juste mettre un de leurs attributs àtruepour les différencier. L'extension permet également de les filtrer lors des SELECT, pour ne pas utiliser des entités « soft-supprimées ».

Uploadable

L'extension Uploadable offre des outils pour gérer l'enregistrement de fichiers associés avec des entités. Elle inclut la gestion automatique des déplacements et des suppressions des fichiers.

IpTraceable

L'extension IpTraceable permet d'assigner l'ip de l'utilisateur courant à un attribut.

Si vous n'avez pas besoin aujourd'hui de tous ces comportements, ayez-les en tête pour le jour où vous en trouverez l'utilité. Autant ne pas réinventer la roue si elle existe déjà ! ;)

Pour conclure

Ce chapitre touche à sa fin et marque la fin de la partie théorique sur Doctrine. Vous avez maintenant tous les outils pour gérer vos entités, et donc votre base de données. Surtout, n'hésitez pas à bien pratiquer, car c'est une partie qui implique de nombreuses notions : sans entraînement, pas de succès !

Le prochain chapitre est un TP permettant de mettre en pratique la plupart des notions abordées dans cette partie.

En résumé

  • Les évènements permettent de centraliser du code répétitif, afin de systématiser leur exécution et de réduire la duplication de code.

  • Plusieurs évènements jalonnent la vie d'une entité, afin de pouvoir exécuter une fonction aux endroits désirés.

  • Les extensions permettent de reproduire des comportements communs dans une application, afin d'éviter de réinventer la roue.

Exemple de certificat de réussite
Exemple de certificat de réussite