laravel-mail-templates maintained by colibri-informatique
Laravel Mail Templates
Modèles d’emails transactionnels éditables depuis le back-office, avec rendu sûr, journal de livraison, prévisualisation et interface Filament optionnelle.
Ce package est né du projet Plateforme de l’Engagement et a été conçu pour être réutilisé tel quel sur d’autres applications Laravel, notamment un futur projet de crowdfunding / crowdlending.
Pourquoi un package dédié ?
Le marché Laravel contient déjà plusieurs briques intéressantes. La revue faite avant extraction :
| Package | Forces | Limite pour notre besoin |
|---|---|---|
spatie/laravel-database-mail-templates (447★, MIT) |
Modèles en base, Mustache, layouts, package maintenu | Orienté Mailable : pas de journal de livraison, pas de back-office, pas de synchronisation de définitions, pas de blocage explicite d’un modèle inactif |
visualbuilder/email-templates (118★, GPL-3.0) |
Éditeur d’emails pour Filament | Licence GPL, édition seulement : pas de journal ni de cycle de vie |
TappNetwork/filament-maillog (30★, MIT) |
Journal des emails dans Filament | Journal uniquement, pas de modèles éditables |
jeffersongoncalves/laravel-mail (11★, MIT) |
Très large : templates, tracking, webhooks, suppression, statistiques | Jeune, faible adoption, périmètre très large et dépendances lourdes |
simplepleb/laravel-email-templates (121★, GPL-3.0) |
Templates Blade | Licence GPL, projet arrêté en 2023, pas de journal |
dansmaculotte/laravel-mail-template (35★, MIT) |
Driver de templates | Projet arrêté en 2022, périmètre limité |
| Laravel Notifications | Livraison multi-canaux native | Mécanisme d’envoi, pas de gestion de modèles ni de journal |
Le module SilverStripe lekoala/silverstripe-email-templates a servi d’inspiration pour plusieurs idées :
- modèles identifiés par un code stable plutôt que par une classe ;
- import/synchronisation de modèles par défaut sans écraser les personnalisations ;
- journal des emails envoyés avec corps conservé ;
- nettoyage périodique des journaux ;
- aperçu et métadonnées (catégorie, variables disponibles, activation/désactivation).
Ce package reprend ces idées, mais reste dans l’écosystème Laravel et utilise Mustache (comme Spatie) plutôt qu’un moteur de template maison.
Fonctionnalités
- Modèles stockés en base, identifiés par un code stable (
auth.magic-link,loan.payment.reminder, etc.). - Rendu Mustache : variables échappées, sections
{{#if variable}}...{{/if}}supportées pour compatibilité. - Refus des variables non échappées (
{{{ ... }}},{{& ... }}) : pas d’injection HTML via une donnée métier. - Aucun rendu Blade du contenu édité : un administrateur ne peut pas exécuter du PHP.
- Modèle absent ou inactif : l’envoi est bloqué et une ligne de journal
skippedest créée. Aucun contenu de repli n’est envoyé silencieusement. - Journal complet :
queued,sending,sent,failed,skipped, tentatives, message-id, erreur, corps rendu, contexte. - Envoi en file avec retries/backoff configurables.
- Prévisualisation et envoi de test depuis le back-office.
- Commande de synchronisation non destructive des modèles par défaut.
- Commande de purge selon une politique de rétention.
- Interface Filament 5 optionnelle (liste, édition, aperçu, test, journal).
- Traductions FR/EN et vues publiables.
- Aucun couplage au domaine métier : le package ne connaît ni
Mission, niParticipation, niLoan.
Installation
Depuis un dépôt local / VCS
{
"repositories": [
{
"type": "path",
"url": "../laravel-mail-templates",
"options": { "symlink": true }
}
]
}
composer require colibri-informatique/laravel-mail-templates:@dev
Le service provider est auto-découvert. Les migrations sont chargées automatiquement ; pour les personnaliser :
php artisan vendor:publish --tag=mail-templates-config
php artisan vendor:publish --tag=mail-templates-migrations
php artisan migrate
Autres publications :
php artisan vendor:publish --tag=mail-templates-views
php artisan vendor:publish --tag=mail-templates-css
php artisan vendor:publish --tag=mail-templates-lang
Commande introuvable : mail-templates:sync
Si Laravel répond The command "mail-templates:sync" does not exist, le service provider n’a pas été chargé. Vérifiez dans cet ordre :
# 1. Le package est-il réellement installé ?
composer show colibri-informatique/laravel-mail-templates
ls vendor/colibri-informatique/laravel-mail-templates/src/MailTemplatesServiceProvider.php
# 2. La découverte des packages est-elle à jour ?
php artisan package:discover
php artisan optimize:clear
# 3. La commande apparaît-elle ?
php artisan list | grep mail-
Causes fréquentes :
- installation avec
composer install --no-scriptsoucomposer require --no-scripts, qui empêchepackage:discoverde s’exécuter ; bootstrap/cache/packages.phpobsolète (le supprimer puis relancerphp artisan package:discover) ;- package listé dans
extra.laravel.dont-discoverdu projet hôte ; - commande lancée depuis un autre répertoire que la racine Laravel.
En dernier recours, enregistrez explicitement le provider.
Laravel 11, 12 et 13 — bootstrap/providers.php :
use ColibriInformatique\MailTemplates\MailTemplatesServiceProvider;
return [
App\Providers\AppServiceProvider::class,
MailTemplatesServiceProvider::class,
];
Laravel 10 — config/app.php, clé providers :
'providers' => [
// ...
ColibriInformatique\MailTemplates\MailTemplatesServiceProvider::class,
],
Aucune définition de modèle
Le message :
Aucune définition de modèle. Renseignez mail-templates.templates ou un provider.
signifie que le package est bien chargé, mais que l’application n’a déclaré aucun modèle. Le package ne livre volontairement aucun contenu par défaut : mail-templates:sync ne crée que les modèles déclarés par le projet.
Diagnostic :
php artisan mail-templates:doctor
La commande indique si la configuration est publiée, combien de modèles sont déclarés, si chaque provider est trouvable et si les tables existent.
Cas 1 — déclaration dans la configuration
php artisan vendor:publish --tag=mail-templates-config
php artisan config:clear
Puis, dans config/mail-templates.php du projet (jamais dans vendor/) :
'templates' => [
'auth.magic-link' => [
'label' => 'Lien de connexion',
'category' => 'Authentification',
'variables' => [
'name' => 'Nom du destinataire',
'url' => 'Lien de connexion',
],
'subject' => 'Votre lien de connexion',
'body' => '<p>Bonjour {{ name }}</p><p><a href="{{ url }}">Se connecter</a></p>',
'preview' => [
'name' => 'Camille',
'url' => 'https://example.test/magic/abc',
],
],
],
Vérification :
php artisan config:clear
php artisan mail-templates:doctor
php artisan mail-templates:sync --dry-run
Cas 2 — déclaration via un provider
Vérifiez les trois points suivants :
-
la classe est autoloadable :
composer dump-autoload -
elle implémente bien
ColibriInformatique\MailTemplates\Contracts\TemplateProvider; -
elle est déclarée dans la configuration du projet :
// config/mail-templates.php 'providers' => [ App\Mail\AppMailTemplates::class, ],
Puis :
php artisan config:clear
php artisan mail-templates:doctor
Depuis la version actuelle, un provider introuvable, un mauvais contrat ou une définition invalide lève une exception explicite au lieu d’être ignoré silencieusement.
Pour inspecter la configuration réellement chargée :
php artisan tinker
config('mail-templates.providers');
config('mail-templates.templates');
app(\ColibriInformatique\MailTemplates\Registry\TemplateRegistry::class)->all();
Déclarer les modèles attendus
Via la configuration
// config/mail-templates.php
'templates' => [
'auth.magic-link' => [
'label' => 'Lien de connexion',
'category' => 'Authentification',
'variables' => [
'name' => 'Nom du destinataire',
'url' => 'Lien de connexion',
],
'subject' => 'Votre lien de connexion',
'body' => '<p>Bonjour {{ name }}</p><p><a href="{{ url }}">Se connecter</a></p>',
'preview' => [
'name' => 'Camille',
'url' => 'https://example.test/magic/abc',
],
],
],
Une variable peut aussi être décrite avec un exemple :
'variables' => [
['name' => 'amount', 'label' => 'Montant', 'example' => '1 250,00 €'],
],
Via un provider
use ColibriInformatique\MailTemplates\Contracts\TemplateProvider;
use ColibriInformatique\MailTemplates\Definitions\TemplateDefinition;
use ColibriInformatique\MailTemplates\Definitions\TemplateVariable;
final class AppMailTemplates implements TemplateProvider
{
public function definitions(): iterable
{
yield new TemplateDefinition(
code: 'loan.payment.reminder',
label: 'Relance de paiement',
category: 'Prêts',
variables: [
new TemplateVariable('first_name', 'Prénom'),
new TemplateVariable('amount', 'Montant', '250,00 €'),
new TemplateVariable('due_date', 'Date d’échéance', '30/11/2026'),
],
subject: 'Échéance à venir le {{ due_date }}',
body: '<p>Bonjour {{ first_name }},</p><p>Votre échéance de {{ amount }} arrive le {{ due_date }}.</p>',
preview: [
'first_name' => 'Camille',
'amount' => '250,00 €',
'due_date' => '30/11/2026',
],
);
}
}
// config/mail-templates.php
'providers' => [
App\Mail\AppMailTemplates::class,
],
Synchroniser les modèles
# Crée les modèles manquants, inactifs, avec le contenu par défaut
php artisan mail-templates:sync
# Crée et active les modèles manquants
php artisan mail-templates:sync --activate
# Écrase aussi les modèles existants (attention aux personnalisations)
php artisan mail-templates:sync --force
# Vérifie sans écrire
php artisan mail-templates:sync --dry-run
# Limite à certains codes
php artisan mail-templates:sync --only=auth.magic-link,loan.payment.reminder
Choix assumé : un modèle synchronisé reste inactif par défaut. Un email ne part jamais sans validation explicite du contenu.
Envoyer un email
use ColibriInformatique\MailTemplates\Services\MailDispatcher;
$log = app(MailDispatcher::class)->queue(
code: 'auth.magic-link',
to: $user->email,
data: [
'name' => $user->full_name,
'url' => $url,
],
context: [
'user_id' => $user->id,
],
);
if ($log->status === \ColibriInformatique\MailTemplates\Enums\MailLogStatus::Skipped) {
// Le modèle est absent ou inactif : l’envoi a été volontairement bloqué.
}
Envoi synchrone (tests, back-office) :
app(MailDispatcher::class)->sendNow('auth.magic-link', $email, $data);
Envoi de test, même si le modèle est inactif :
app(MailDispatcher::class)->sendTest('auth.magic-link', 'admin@example.test');
Cycle de vie du journal
| Statut | Signification |
|---|---|
queued |
Le mailable est en file d’attente. |
sending |
Le transport a commencé l’envoi (événement MessageSending). |
sent |
Le transport a accepté le message (MessageSent). |
failed |
Échec définitif du job ou exception d’envoi. |
skipped |
Modèle absent ou inactif : aucun email n’a été envoyé. |
Le journal est mis à jour via les en-têtes X-Mail-Template-Code et X-Mail-Template-Log-Id, ce qui évite de désérialiser le payload du job et reste robuste aux évolutions de Laravel.
Purge :
php artisan mail-logs:prune
php artisan mail-logs:prune --days=30
php artisan mail-logs:prune --dry-run
Le corps des emails peut contenir des données personnelles. La rétention par défaut est de 90 jours ; adaptez-la à votre politique RGPD.
Interface Filament
Ajoutez le plugin à votre panel :
use ColibriInformatique\MailTemplates\Filament\MailTemplatesPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(MailTemplatesPlugin::make());
}
L’interface apporte :
- Modèles d’emails : liste, édition de l’objet et du contenu, activation, aide sur les variables, aperçu, envoi de test.
- Journal des emails : liste filtrable, page de détail, aperçu du corps, erreurs et message-id.
Le corps des emails est affiché dans une iframe sandboxée : le CSS de l’email ne peut pas casser l’interface Filament et les scripts éventuellement présents dans le HTML ne sont pas exécutés.
Les libellés sont traduits en français et en anglais. Le groupe de navigation est configurable via mail-templates.ui.navigation_group.
L’interface ne crée pas de modèles : les codes sont définis par le développeur. Utilisez mail-templates:sync pour créer les lignes manquantes.
Sécurité du rendu
- Le contenu édité est rendu par Mustache, jamais par Blade.
{{ variable }}est toujours échappé (htmlspecialchars).- Les formes non échappées sont refusées par défaut :
{{{ variable }}}{{& variable }}
- Vous pouvez les autoriser explicitement avec
allow_unescaped_variables, mais ce n’est pas recommandé. - Les variables inconnues sont rendues vides, comme avec Mustache.
- Un modèle inactif ne produit aucun envoi de repli.
- Le contenu HTML reste édité par un administrateur de confiance ; limitez l’accès à l’écran Filament aux rôles appropriés (le package ne fournit pas de policy par défaut).
Configuration
| Clé | Défaut | Rôle |
|---|---|---|
templates |
[] |
Définitions déclaratives. |
providers |
[] |
Classes TemplateProvider. |
table_names.templates |
mail_templates |
Table des modèles. |
table_names.logs |
mail_logs |
Table des journaux. |
layout |
mail-templates::layout |
Vue de layout. |
css |
null |
Chemin CSS personnalisé ; sinon CSS du package. |
inline_css |
true |
Inline le CSS dans le HTML final. |
allow_unescaped_variables |
false |
Autorise {{{ ... }}} et {{& ... }}. |
queue.tries |
5 |
Tentatives du mailable. |
queue.backoff |
[10,30,60,300,900] |
Délais entre tentatives. |
logs.store_body |
true |
Conserve le corps rendu dans le journal. |
logs.retention_days |
90 |
Rétention par défaut de mail-logs:prune. |
ui.enabled |
true |
Active les écrans Filament. |
ui.navigation_group |
Administration |
Groupe de navigation Filament. |
Tests
composer install
composer test
composer lint
composer phpstan
Le package utilise Orchestra Testbench, Pest, SQLite en mémoire et Mail::fake().
Migration depuis un existant
Le package a été extrait d’une application qui utilisait :
- un enum
EmailTemplateCodeavec 22 cas ; - un
EmailServiceet unTemplateEmail; - des tables
email_templatesetemail_logs; - des écouteurs de queue dans
AppServiceProvider.
Pour migrer :
- Déclarer les 22 modèles dans un
TemplateProvider(ou dans la config). - Remplacer les appels
EmailService::queue()parMailDispatcher::queue(). - Supprimer les écouteurs
JobProcessing/MessageSent/JobExceptionOccurredde l’application : le package les gère via les en-têtes du message. - Transférer les contenus existants vers
mail_templates(un import legacy est prévu en v0.2). - Supprimer les anciens modèles, migrations, vues et ressources Filament.
Les notifiers métier (ParticipationCancelledNotifier, etc.) restent dans l’application : ils appellent simplement le dispatcher.
Feuille de route
- v0.2 : commande d’import depuis une table legacy, versioning des modèles, modèles par locale.
- v0.3 : adaptateur Laravel Notifications (
TemplateNotification), canal SMS, relances idempotentes. - v0.4 : policies Filament, permissions par rôle, aperçu MJML éventuel.
Le package reste volontairement séparé d’un futur laravel-notifications : les templates et le journal sont une capacité stable, les relances et les canaux pourront s’appuyer dessus.
Licence
MIT.