Licence CC BY-NC-SA

Les évènements et extensions Doctrine

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'un article, à chaque modification de l'entité Article 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 lève à 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é Article, qui va comprendre un attribut $dateEdition représentant la date de la dernière édition de l'article. 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 commande doctrine:schema:update.

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'annotation HasLifecycleCallbacks dans le namespace habituel des annotations Doctrine :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Article
{
  // …
}

Cette annotation permet à Doctrine de vérifier les callbacks éventuels contenus dans l'entité. 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éthode updateDate() dans l'entité Article. Cette méthode doit définir l'attribut $dateEdition à la date actuelle, afin de mettre à jour automatiquement la date d'édition d'un article. Voici à quoi elle pourrait ressembler :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
// src/Sdz/BlogBundle/Entity/Article.php

/**
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Article
{
  // …

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

Maintenant il faut dire à Doctrine d'exécuter cette méthode (ce callback) dès que l'entité Article 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ènement PreUpdate : 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 :

1
2
3
4
5
6
<?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 un article, le modifier, et l'enregistrer (avec un flush()), vous verrez que l'attribut $dateEdition va se mettre à jour automatiquement. Attention, l'évènement update n'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ènement PreUpdate, il existe l'évènement postUpdate et 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.

Méta-évènement

Évènement

Description

Persist

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é.

Persist

PostPersist

L'évènement postPersist se produit juste après que l'EntityManager a 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().

Update

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 _callbac_k 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.

Update

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.

Remove

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().

Remove

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.

Load

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é.

Attention, ces évènements se produisent lorsque vous créez et modifiez vos entités en manipulant les objets. Ils ne sont pas déclenchés lorsque vous effectuez des requêtes DQL ou avec le QueryBuilder. Car ces requêtes peuvent toucher un grand nombre d'entités et il serait dangereux pour Doctrine de déclencher les évènements correspondants un à un.

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 commentaires pour les articles du blog.

L'idée est la suivante : nous avons un blog très fréquenté, et un petit serveur. Au lieu de récupérer le nombre de commentaires des articles à l'aide d'une requête COUNT(*), on décide de rajouter un attribut nbCommentaires à notre entité Article. 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'un commentaire, on doit effectuer un +1 au compteur de l'Article lié ;
  • À chaque suppression d'un commentaire, on doit effectuer un -1 au compteur de l'Article lié.

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'un commentaire. Il s'agit des évènements PrePersist et PreRemove. Pourquoi ? Car les évènements *Update sont déclenchés à la mise à jour d'un Commentaire, ce qui ne change pas notre compteur ici. Et les évènements Post* 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.

Au final, voici ce que nous devons rajouter dans l'entité Commentaire :

 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
<?php
// src/Sdz/Blog/Bundle/Entity/Commentaire.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CommentaireRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Commentaire
{
  /**
   * @ORM\prePersist
   */
  public function increase()
  {
    $nbCommentaires = $this->getArticle()->getNbCommentaires();
    $this->getArticle()->setNbCommentaires($nbCommentaires+1);
  }

  /**
   * @ORM\preRemove
   */
  public function decrease()
  {
    $nbCommentaires = $this->getArticle()->getNbCommentaires();
    $this->getArticle()->setNbCommentaires($nbCommentaires-1);
  }

  // ...
}

Cette solution est possible car nous avons une relation entre ces deux entités Commentaire et Article, il est donc possible d'accéder à l'article depuis un commentaire.

Bien entendu, il vous faut rajouter l'attribut $nbCommentaires dans l'entité Article si vous ne l'avez pas déjà.

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 les articles d'un blog, les sujets d'un forum, etc. 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 StofDoctrineExtensionBundle. Commençons par l'installer avec Composer, rajoutez cette dépendance dans votre composer.json :

1
2
3
4
5
// composer.json

"require": {
  "stof/doctrine-extensions-bundle": "dev-master"
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?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 attribut slug dans 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 configuration config.yml. Rajoutez donc cette section :

1
2
3
4
5
6
7
# app/config/config.yml

# Stof\DoctrineExtensionBundle 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 simplement Slug. En l'occurrence, il faut ajouter un nouvel attribut slug (le nom est arbitraire) dans votre entité, sur lequel nous mettrons l'annotation. Voici un exemple dans notre entité Article :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

class Article
{
  // …

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

  // …
}

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

Ensuite, l'annotation Slug s'utilise très simplement sur un attribut qui va contenir le slug. L'option fields permet 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.

N'oubliez pas de mettre à jour votre base de données avec la commande doctrine:schema:update, mais également de générer le getter et le setter du slug, grâce à la commande generate:doctrine:entities SdzBlogBundle:Article.

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 attribut slug sera automatiquement rempli ! Par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
// Dans un contrôleur

public function testAction()
{
  $article = new Article();
  $article->setTitre("L'histoire d'un bon weekend !");

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

  return new Response('Slug généré : '.$article->getSlug()); // Affiche « Slug généré : l-histoire-d-un-bon-weekend »
}

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

Vous avez pu remarquer que j'ai défini l'attribut slug comme unique (unique=true dans l'annotation Column). En effet, dans le cadre d'un article de blog, on se sert souvent du slug comme identifiant de l'article, afin de l'utiliser dans les URL et améliorer le référencement. Sachez que l'extension est intelligente : si vous ajouter un Article avec un titre qui existe déjà, le slug sera suffixé de « -1 » pour garder l'unicité, par exemple « un-super-weekend-1 ». Si vous ajoutez un troisième titre identique, alors le slug sera « un-super-weekend-2 », etc.

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

Liste des extensions Doctrine

Voici la liste de toutes les 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 type date dans 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 la même chose que ce qu'on a fait dans le paragraphe précédent sur les évènements Doctrine, et en mieux !

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 à true pour 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.

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.