Symfony, Doctrine et Triggers SQL : une tentative de réconciliation
Publié le 27 août 2025
Tout commence avec une situation familière : une logique métier critique, l’audit d'une modification, la mise à jour d'un compteur ou la validation d'une donnée complexe qu’on souhaite exécuter de manière atomique et infaillible à chaque changement en base de données : la solution technique la plus robuste est souvent un trigger SQL. Pourtant on n'ose pas toujours les utiliser dans un projet Symfony tant leur gestion peut vite devenir un vrai casse-tête. Le code SQL se perd car non versionné ou caché au fin fond d’une migration et le développeur qui modifie une entité Doctrine n'a parfois aucune idée de la logique qui se déclenche en base de données.
Ce fossé entre notre application PHP et notre base de données crée une dette technique insidieuse. On finit par réimplémenter côté applicatif des logiques qui seraient bien plus robustes et performantes au niveau de la base de données. C'est pour tenter de mettre fin à cette rupture que le Trigger Mapping Bundle a été créé. L'idée est simple : essayer de réconcilier nos applications Symfony avec les triggers SQL.

Anatomie des Triggers SQL
Avant de plonger dans la solution, un bref rappel sur leur fonctionnement.
Un trigger est un bloc de code métier (en l'occurrence une requête SQL) qui s'exécute automatiquement lors d’un événement spécifique sur une table ou une vue. Le fondement d'un trigger est qu'il est automatique : il n'est pas appelé directement par l'application, mais "déclenché" par le SGBD lui-même. Pour résumer : quoi qu’il se passe (ou qu’il ne se passe pas) dans votre application ou dans n’importe quel autre service branché sur votre BDD, si un trigger écoute les insertions dans une table (par exemple), il se déclenchera systématiquement.
Sa syntaxe de base s'articule autour de trois composants clés :
- Le déclencheur (
on
) : L'action qui active le trigger. Il s'agit généralement d'une opération de manipulation de données (INSERT
,UPDATE
, ouDELETE
). - Le moment d'exécution (
when
) : Précise si le trigger doit s'exécuter juste avant (BEFORE
) ou juste après (AFTER
) que l'événement ait eu lieu. - La table cible : La table sur laquelle le trigger est attaché.
La puissance des triggers réside aussi (et surtout) dans les notions OLD
et NEW
. Ces structures, disponibles dans le corps du trigger, permettent d'accéder aux données de la ligne affectée par l'opération.
NEW
contient les valeurs de la ligne après la modification (pour unINSERT
ou unUPDATE
).OLD
contient les valeurs de la ligne avant la modification (pour unUPDATE
ou unDELETE
).
Cette capacité à comparer l'état avant et après est au cœur de la plupart des logiques de triggers, permettant par exemple de vérifier si un champ spécifique a changé (OLD.price
<> NEW.price
) ou d'archiver les anciennes données avant leur suppression.
Les triggers sont puissants, presque parfaits lorsque les modifications en BDD peuvent survenir à la fois de notre application mais aussi d’un script extérieur ou d’une migration SQL. Cependant ils ne sont pas sans défauts : un trigger mal conçu, trop complexe, ou une cascade de triggers peuvent sérieusement ralentir les opérations. Par ailleurs, ils ont tendance à devenir “invisibles” dans nos projets Symfony et confinés au schéma de la BDD.
Les grands oubliés de nos projets
Dans un projet Symfony/Doctrine classique, il n'y a pas vraiment de pont entre une entité et un trigger. Cette absence est plutôt logique : un ORM comme Doctrine vise à être agnostique de la base de données pour garantir sa portabilité.
On se retrouve alors avec des solutions un peu bancales, on peut les mettre en place dans des migrations Doctrine en ajoutant un CREATE TRIGGER mais la syntaxe est souvent problématique à cause des délimiteurs, et surtout, la logique du trigger est noyée dans un fichier qui sera vite oublié.
La solution préférée est généralement de se tourner vers les événements de cycle de vie de Doctrine (prePersist, postUpdate, etc.). C'est une bonne solution pour de la logique applicative, mais ce ne sont pas des triggers. Leur exécution n'est pas garantie si une modification est faite en dehors de votre application (via un script, un autre service, ou directement en BDD), et ils ne bénéficient pas de la même atomicité que le SGBD.
Une tentative de solution : le mapping déclaratif avec les attributs PHP
Le Trigger Mapping Bundle souhaite changer la donne en introduisant une approche déclarative, directement inspirée de la façon dont Doctrine mappe les entités et leurs propriétés.
Fini les fichiers SQL perdus, tout se passe désormais au-dessus de votre classe d'entité grâce à un attribut PHP :
namespace App\Entity;
use Talleu\TriggerMapping\Attribute\Trigger;
use App\Triggers\MyAwesomeTrigger;
#[Trigger(
name: 'trg_user_updated_at',
when: 'AFTER',
on: ['INSERT', 'UPDATE'],
function: 'fn_update_timestamp_func',
className: MyAwesomeTrigger::class
)]
class User
{
// ...
}
Les avantages sont immédiats :
- Visibilité : N'importe quel dev ouvrant le fichier User.php voit instantanément qu'un trigger est associé à cette entité. La logique n'est plus cachée.
- Versioning : L'attribut fait partie du code PHP, il est donc versionné avec Git. L'historique des modifications de vos triggers est aussi clair que celui de vos entités.
On passe d'une approche impérative ("exécute ce SQL") à une approche déclarative ("voici à quoi doit ressembler l'état final"). C'est la philosophie de Doctrine, étendue aux triggers.
Une boîte à outils pour vous simplifier la vie
Le bundle ne se contente pas de mapper, il vient avec une série de commandes pour gérer le cycle de vie de vos triggers.
bin/console triggers:mapping:update
C'est la commande “magique” pour les projets qui ont déjà des triggers en production. Elle scanne votre base de données, trouve les triggers pas encore mappés par le bundle, et avec les bonnes options (--apply --create-files), elle va :
- Identifier l'entité Doctrine correspondante.
- Ajouter automatiquement l'attribut #[Trigger] sur la classe PHP.
- Extraire le code SQL du trigger et le sauvegarder dans un fichier local (par défaut dans une classe PHP), prêt à être versionné.
En une seule commande, vous pouvez absorber toute votre dette technique.
bin/console make:trigger
Le moyen le plus simple de commencer. Intégrée au MakerBundle de Symfony, cette commande lance un assistant interactif qui vous guide pour créer un nouveau trigger. Il vous pose les bonnes questions (sur quelle entité, quel nom, quels événements...) et génère pour vous l'attribut sur l'entité et le fichier de logique correspondant (.sql ou classe PHP).
bin/console triggers:schema:diff
Vous préférez écrire votre mapping d'abord? Ajoutez l'attribut #[Trigger] sur votre entité, puis lancez cette commande. Elle générera le fichier de logique vide et la migration Doctrine nécessaire pour créer le trigger en base de données.
bin/console triggers:schema:validate
Cette commande est un filet de sécurité à intégrer dans votre CI. Elle compare les triggers déclarés dans votre code avec ceux réellement présents en base de données et vous alerte des potentielles incohérences. Fini les mauvaises surprises en production.
Attention le bundle ne compare pour le moment pas l’intégralité du contenu des fonctions exécutées par les triggers, il compare les fonctions appelées, la table, le timing, et globalement tout ce qui concerne la partie “définition” du trigger.
bin/console triggers:schema:update
Cette commande est la dernière étape du déploiement de vos triggers. Son objectif est de récupérer les définitions et le contenu des fonctions de tous les triggers déclarés sur vos entités et de les exécuter directement sur la base de données.
Mise en route rapide
L'installation se fait en deux temps trois mouvements :
- Installation via Composer :
composer require talleu/trigger-mapping
- Configuration (optionnel) : Créez un fichier
config/packages/trigger_mapping.yaml
pour définir où stocker des infos de configuration (format des triggers, emplacement des fichiers, etc…)
# config/packages/trigger_mapping.yaml
trigger_mapping:
storage:
type: 'php'
namespace: 'App\Triggers'
Et voilà, le bundle est prêt à être utilisé.
Trigger ou Listener Doctrine : que choisir ?
Maintenant que les triggers sont faciles à gérer, la question n'est plus "peut-on les utiliser?" mais "quand doit-on les utiliser?". Voici un guide pour vous aider à décider entre un trigger de base de données (géré par le bundle) et un listener d'entité Doctrine.

Le Trigger Mapping Bundle ne réinvente pas la roue, il souhaite construire le pont qui manquait entre nos applications Symfony et la puissance de nos bases de données, en rendant les triggers visibles, versionnables et gérables.
Vous pouvez désormais choisir le bon outil pour le bon travail, sans compromis. Pour une logique d'intégrité liée à la donnée, utilisez un trigger. Pour une logique métier liée à l'application, gardez vos listeners.
Le projet est open-source et n'attend que vos retours et contributions !