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 |
---|---|---|
|
|
L'évènement |
|
|
L'évènement |
|
|
L'évènement |
|
|
L'évènement |
|
|
L'évènement |
|
|
L'évènement |
|
|
L'évènement |
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 |
---|---|
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. |
|
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. |
|
L'extension Sluggable permet de générer automatiquement un slug à partir d'attributs spécifiés. |
|
L'extension Timestampable automatise la mise à jour d'attributs de type |
|
L'extension Loggable permet de conserver les différentes versions de vos entités, et offre des outils de gestion des versions. |
|
L'extension Sortable permet de gérer des entités ordonnées, c'est-à-dire avec un ordre précis. |
|
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 à |
|
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.