declare(strict_types=1); en haut d’un fichier ne suffit pas à empêcher qu’une traduction vide traverse une couche métier. Le typage strict de PHP vérifie les types, pas le contenu : une chaîne vide reste une chaîne valide au sens du langage, et rien n’empêche '' de passer un paramètre déclaré string.
Sur un projet de fiches produits multilingues géré par une couche de traduction maison (sans WPML ni Polylang, pour des raisons de performance sur un catalogue volumineux), ce trou a laissé passer des dizaines de fiches avec un titre traduit vide en néerlandais, publiées telles quelles pendant plusieurs semaines. La correction est passée par un objet dédié qui refuse la valeur invalide dès sa construction, plutôt qu’un contrôle dispersé dans chaque contrôleur.
Le symptôme : une chaîne vide qui passe tous les contrôles habituels
Le pipeline d’import associait chaque produit à un tableau de traductions indexé par langue. Le contrôleur vérifiait isset($traductions[$langue]), ce qui est vrai même quand la valeur associée est une chaîne vide. Le champ finissait enregistré tel quel dans la table de traduction, et le gabarit affichait un titre de produit invisible sur la page en langue néerlandaise.
Le typage strict de PHP (declare(strict_types=1)) garantit qu’un appel de fonction respecte les types déclarés dans la signature, sans conversion implicite. Il empêche par exemple de passer un entier à un paramètre string sans conversion explicite. Il ne garantit en revanche aucune contrainte sur le contenu d’une chaîne : longueur, absence d’espaces, non-vacuité. C’est un outil de cohérence de types, pas de validation métier.
Le correctif : un value object qui interdit la construction avec une chaîne vide

La solution retenue introduit une petite classe immuable, TraductionNonVide, dont le constructeur lève une exception si la valeur reçue est vide après trim(). Toute la couche métier manipule ensuite des instances de cette classe plutôt que des chaînes brutes, ce qui déplace le contrôle à un seul endroit au lieu de le disperser dans chaque contrôleur ou chaque script d’import.
declare(strict_types=1);
final class TraductionNonVide
{
private string $valeur;
public function __construct(string $valeur)
{
$nettoyee = trim($valeur);
if ($nettoyee === '') {
throw new InvalidArgumentException(
'Une traduction ne peut pas être une chaîne vide.'
);
}
$this->valeur = $nettoyee;
}
public function valeur(): string
{
return $this->valeur;
}
}
Le champ de la fiche produit qui reçoit le titre traduit accepte désormais un TraductionNonVide dans sa signature, jamais un string nu :
declare(strict_types=1);
final class FicheProduit
{
private TraductionNonVide $titre;
public function definirTitre(TraductionNonVide $titre): void
{
$this->titre = $titre;
}
}
Grâce au typage strict, il devient impossible d’appeler definirTitre() avec une chaîne brute : PHP refuse la conversion implicite d’un string vers un objet, et l’erreur remonte immédiatement à l’endroit où la traduction est construite, pas trois couches plus loin dans le gabarit d’affichage.
Où placer le contrôle dans le pipeline d’import
Le point d’entrée naturel est la fonction qui lit le fichier d’import (CSV ou JSON selon les fournisseurs) et construit les objets métier. C’est là, et uniquement là, que la conversion d’une chaîne brute en TraductionNonVide doit avoir lieu :
- Lecture de la ligne d’import pour la langue concernée.
- Tentative de construction du
TraductionNonVidecorrespondant. - Si l’exception est levée, la ligne est mise de côté dans un journal d’erreurs d’import plutôt que silencieusement ignorée.
- Le reste du pipeline ne manipule plus jamais de chaîne brute pour ce champ.
Cette approche évite un piège classique : ajouter un simple if (empty($traduction)) continue; dans le contrôleur d’affichage. Ce genre de garde-fou masque le symptôme (la page ne montre plus de titre vide) sans corriger la cause (l’import a quand même laissé passer une donnée incomplète), et surtout sans informer personne qu’une traduction manque.
Le test unitaire qui aurait dû exister depuis le début
Avant ce correctif, la suite de tests couvrait la construction d’une fiche produit avec des traductions valides, mais aucun test ne vérifiait le comportement face à une chaîne vide. Le test ajouté est volontairement minimal :
declare(strict_types=1);
final class TraductionNonVideTest extends PHPUnit\Framework\TestCase
{
public function testRefuseUneChaineVide(): void
{
$this->expectException(InvalidArgumentException::class);
new TraductionNonVide('');
}
public function testRefuseUneChaineDEspaces(): void
{
$this->expectException(InvalidArgumentException::class);
new TraductionNonVide(' ');
}
public function testAccepteUneTraductionValide(): void
{
$traduction = new TraductionNonVide('Chaussures de randonnée');
$this->assertSame('Chaussures de randonnée', $traduction->valeur());
}
}
Le deuxième cas, la chaîne composée uniquement d’espaces, est le plus souvent oublié : un export mal nettoyé depuis un tableur peut très bien contenir une cellule qui semble vide à l’œil mais qui contient un espace insécable ou une tabulation. Sans le trim() dans le constructeur, ce cas précis aurait continué à passer.
Sur ce genre de couche multilingue maison, mieux vaut un objet qui refuse de naître avec une valeur incohérente qu’une fonction qui accepte tout et laisse le problème remonter trois écrans plus loin.
Les limites de cette approche
Le value object protège contre la chaîne vide, mais pas contre une traduction de mauvaise qualité (un simple copier-coller du texte source, par exemple). Détecter qu’une traduction en néerlandais est en réalité identique au texte français demanderait une comparaison de contenu entre langues, un sujet distinct qui touche à la qualité de la traduction plutôt qu’à sa présence. De même, cette recette ne remplace pas une validation côté formulaire d’administration : elle protège la couche métier, pas l’interface de saisie, qui mérite ses propres messages d’erreur à l’utilisateur au moment de la saisie.
Elle reste en revanche peu coûteuse à mettre en place : une seule classe, deux tests, et un point d’entrée unique dans le pipeline d’import. Sur un projet où la couche de traduction est déjà maison, c’est le niveau de rigueur minimal avant de considérer que le système est fiable.
En résumé
Le typage strict de PHP garantit la cohérence des types, pas la validité du contenu : une chaîne vide reste un string valide. La solution durable consiste à encapsuler la traduction dans un objet qui refuse de se construire avec une valeur vide, plutôt que de multiplier les vérifications empty() disséminées dans le code. Ce déplacement du contrôle, de la périphérie vers le cœur du modèle, est ce qui a permis d’éliminer durablement ce type d’incident sur ce projet.