Le problème
Quelqu'un commente sur le blog. Tu l'apprends... quand tu penses à vérifier manuellement. Pas de base de données, pas de panel d'administration. Le seul moyen de savoir, c'est d'ouvrir les fichiers JSON un par un. C'est viable exactement jusqu'au moment où ça ne l'est plus.
La correction est simple : envoyer un email à chaque nouveau commentaire. Pas de système de modération, pas d'interface, juste une notification qui dit "quelqu'un a posté quelque chose, va voir". Ce qui suit détaille comment mettre ça en place avec PHPMailer, sans Composer, en 40 lignes de wrapper.
Trois options, un seul choix
mail() natif — dépend de la config sendmail/postfix de l'hôte,
non fiable sur hébergement mutualisé, atterrit quasiment systématiquement en spam. Sur un
serveur dédié sans SPF/DKIM correctement configuré, oublie. Même avec, le comportement
varie selon l'environnement : ce qui fonctionne en local ne fonctionne pas en prod, et
inversement.
Client SMTP maison via fsockopen — fragile. La négociation TLS
est pénible à faire correctement, la gestion des codes de retour SMTP encore plus. Tu finis
par réimplémenter ce que PHPMailer gère déjà depuis 2001. La vie est courte.
PHPMailer — testé en production depuis deux décennies, gère l'encodage, TLS, les timeouts, les erreurs d'authentification. Ça tient en 3 fichiers à copier. Pas besoin de Composer. C'est le choix évident.
Installer PHPMailer sans Composer
L'installation se fait à la main : aller sur le dépôt GitHub de PHPMailer, dans le répertoire
src/, et copier trois fichiers dans le projet.
blog/
├── lib/
│ └── PHPMailer/
│ ├── Exception.php
│ ├── PHPMailer.php
│ └── SMTP.php
├── notify.php
├── comment-handler.php
└── ...
Pas de vendor/, pas d'autoload.php. Trois require_once
et les namespaces, c'est tout. Dans notify.php :
<?php
require_once __DIR__ . '/lib/PHPMailer/Exception.php';
require_once __DIR__ . '/lib/PHPMailer/PHPMailer.php';
require_once __DIR__ . '/lib/PHPMailer/SMTP.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
Lors d'une mise à jour de PHPMailer, il suffira de remplacer ces trois fichiers. Pas de
composer update à lancer, pas de lockfile à committer, pas de dépendances
transitives à surveiller.
Le wrapper : notify.php
Une seule fonction publique, notify_new_comment(), qui prend en paramètre
les données du commentaire et l'URL de l'article :
<?php
require_once __DIR__ . '/lib/PHPMailer/Exception.php';
require_once __DIR__ . '/lib/PHPMailer/PHPMailer.php';
require_once __DIR__ . '/lib/PHPMailer/SMTP.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
/**
* Envoie une notification email lors d'un nouveau commentaire.
* Pattern best-effort : retourne false silencieusement en cas d'erreur.
*
* @param array $comment Données du commentaire (author, content, date)
* @param string $slug Slug de l'article commenté
* @param string $title Titre de l'article (fallback : slug)
* @return bool
*/
function notify_new_comment(array $comment, string $slug, string $title = ''): bool
{
// Guard clause : pas de config = pas de notif, pas de crash
if (!defined('SMTP_HOST') || !defined('NOTIFY_TO')) {
return false;
}
$excerpt = mb_substr($comment['content'] ?? '', 0, 300, 'UTF-8');
$author = $comment['author'] ?? 'Anonyme';
$date = $comment['date'] ?? date('Y-m-d H:i');
$label = $title ?: $slug;
$url = (defined('SITE_URL') ? SITE_URL : '') . '/blog/' . $slug;
$body = "Nouveau commentaire sur : {$label}\n";
$body .= "URL : {$url}\n\n";
$body .= "Auteur : {$author}\n";
$body .= "Date : {$date}\n\n";
$body .= "---\n";
$body .= $excerpt;
if (mb_strlen($comment['content'] ?? '', 'UTF-8') > 300) {
$body .= "\n[...]";
}
try {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USER;
$mail->Password = SMTP_PASS;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->CharSet = 'UTF-8';
$mail->setFrom(SMTP_USER, 'Blog Notification');
$mail->addAddress(NOTIFY_TO);
$mail->isHTML(false);
$mail->Subject = "[Blog] Nouveau commentaire : {$label}";
$mail->Body = $body;
$mail->send();
return true;
} catch (Exception $e) {
error_log('[notify_new_comment] SMTP error for slug=' . $slug . ' : ' . $e->getMessage());
return false;
}
}
Quelques décisions qui méritent d'être explicitées :
-
Guard clause en tête — si
SMTP_HOSTouNOTIFY_TOne sont pas définis, la fonction retournefalsesans bruit. Utile en développement local oùconfig.local.phpn'existe pas. -
ENCRYPTION_STARTTLSsur le port 587 — c'est le port standard pour la soumission SMTP avec STARTTLS. Gmail et la quasi-totalité des providers le supportent. Le port 465 (SSL implicite) existe aussi mais STARTTLS est plus standard aujourd'hui. -
isHTML(false)— du texte brut suffit pour une notification interne. Pas besoin de template HTML, pas de risque d'injection, pas de client qui rend le HTML différemment. -
mb_substr— tronque à 300 caractères en UTF-8. Sansmb_, tu risques de couper au milieu d'un caractère multi-octets et de corrompre l'encodage du mail. -
error_logsur exception — jamais d'erreur visible pour l'utilisateur. L'échec d'envoi d'email ne doit pas se propager vers le navigateur. - Le retour booléen est ignoré par l'appelant — c'est délibéré. C'est le pattern best-effort : la notification est accessoire, le commentaire est l'opération principale.
Config Gmail : App Password
Gmail bloque les connexions avec identifiant/mot de passe standard depuis 2022. Il faut générer un App Password (mot de passe d'application) :
- Aller sur Compte Google → Sécurité
- Activer la validation en deux étapes si ce n'est pas fait (prérequis)
- Chercher App Passwords (ou "Mots de passe des applications")
- Générer un mot de passe pour "Mail" — tu obtiens 16 caractères
- Utiliser ces 16 caractères comme
SMTP_PASS
Pourquoi pas OAuth2 ? Parce qu'OAuth2 pour SMTP implique : créer un projet Google Cloud, obtenir un client ID et un client secret, gérer un refresh token, écrire la logique de renouvellement. Pour un blog personnel qui envoie deux emails par semaine, c'est disproportionné. L'App Password est le bon compromis ici.
La configuration va dans config.local.php, gitignorée :
<?php
// config.local.php — NE PAS COMMITTER
define('SMTP_HOST', 'smtp.gmail.com');
define('SMTP_USER', 'ton-adresse@gmail.com');
define('SMTP_PASS', 'abcd efgh ijkl mnop'); // App Password Gmail (16 chars)
define('NOTIFY_TO', 'ton-adresse@gmail.com');
Et le fichier config.local.example.php est committé comme template pour ne
pas avoir à documenter la structure ailleurs :
<?php
// config.local.example.php — Copier en config.local.php et remplir les valeurs
define('SMTP_HOST', 'smtp.gmail.com');
define('SMTP_USER', 'votre-email@gmail.com');
define('SMTP_PASS', 'app-password-16-chars');
define('NOTIFY_TO', 'votre-email@gmail.com');
Dans .gitignore :
config.local.php
Intégration dans comment-handler.php
La notification s'appelle après file_put_contents, jamais avant. Si l'email
échoue, le commentaire est déjà sauvegardé. C'est tout le sens du pattern best-effort :
l'opération principale (sauvegarder le commentaire) ne doit jamais échouer à cause d'une
opération secondaire (envoyer une notification).
<?php
require_once __DIR__ . '/../config.php';
if (file_exists(__DIR__ . '/../config.local.php')) {
require_once __DIR__ . '/../config.local.php';
}
require_once __DIR__ . '/notify.php';
// ... validation, sanitisation du commentaire ...
$comment = [
'author' => $author,
'content' => $content,
'date' => date('Y-m-d H:i'),
];
// Opération principale : sauvegarder le commentaire
$file = __DIR__ . '/data/' . $slug . '.json';
$comments = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
$comments[] = $comment;
file_put_contents($file, json_encode($comments, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Opération secondaire : notifier (best-effort, on ignore le retour)
$title = extract_article_title($slug);
notify_new_comment($comment, $slug, $title);
header('Location: /blog/' . $slug . '#comments');
exit;
/**
* Lit le fichier de l'article et extrait le contenu de la balise h1.
* Retourne le slug en fallback si le parsing échoue.
*/
function extract_article_title(string $slug): string
{
$file = __DIR__ . '/posts/' . $slug . '.php';
if (!file_exists($file)) {
return $slug;
}
$content = file_get_contents($file);
if (preg_match('#<h1[^>]*>(.+?)</h1>#s', $content, $matches)) {
return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8');
}
return $slug;
}
L'extraction du titre lit le fichier PHP de l'article et cherche la première balise
<h1>. Si le fichier n'existe pas ou si le regex ne matche pas (article en
cours d'écriture, structure différente), on retombe sur le slug. Pas d'exception, pas de
blocage.
Sécurité
-
Credentials dans
config.local.php, gitignorée — jamais dans le dépôt.config.local.example.phpest committé comme référence, sans valeurs réelles. -
Aucune entrée utilisateur dans les headers email — le nom de l'auteur
et le contenu du commentaire vont uniquement dans le body, jamais dans
From,To,Subjectou tout autre header. Ça élimine le risque d'injection de headers SMTP. -
error_log, pas d'affichage utilisateur — les erreurs SMTP (mot de passe révoqué, quota dépassé, timeout) vont dans les logs du serveur, pas dans la réponse HTTP. Pas de fuite d'information sur la config SMTP. - L'App Password peut être révoqué indépendamment — si le mot de passe d'application est compromis, on le supprime depuis le compte Google sans toucher au mot de passe principal ni aux autres applications.
Limites
Gmail rate limit à 500 emails/jour — un blog personnel n'atteindra jamais ça. Si tu traites 500 commentaires par jour, tu as d'autres problèmes à régler en priorité.
La connexion SMTP ajoute ~1-2 secondes au POST — acceptable ici puisque la réponse est un redirect de toute façon, l'utilisateur ne voit pas ce délai. Si la latence devenait un problème, il faudrait passer par une queue (un fichier de tâches, un job cron, Redis). Pour deux commentaires par semaine, c'est du sur-engineering caractérisé.
Le port 587 est bloqué sur certains hébergements mutualisés — dans ce cas,
PHPMailer ne peut pas se connecter et notify_new_comment() retournera toujours
false. Les solutions : utiliser le provider SMTP de l'hébergeur, passer par
un service transactionnel (Mailgun, Brevo), ou rester sur mail() natif si
l'hébergeur le configure correctement.
Pas de queue asynchrone — si Gmail est lent (rare mais possible), l'utilisateur attend. Pour un blog personnel, ajouter une queue de tâches pour ça serait absurde. Le délai de redirect absorbe la latence dans la grande majorité des cas.
40 lignes de wrapper, 3 fichiers copiés depuis PHPMailer, un config.local.php
gitignorée. C'est tout. Les parties difficiles — TLS, authentification, encodage, gestion
des erreurs SMTP — sont le problème de PHPMailer, pas le tien.