Une extension de tarification pour un site de location de matériel manipulait ses prix sous forme de simples tableaux associatifs : array( 'montant' => 42.5, 'devise' => 'EUR' ). Au fil des mois, plusieurs fonctions différentes ont commencé à produire ce même tableau, chacune avec de légères variations : l’une omettait parfois la clé devise quand elle valait EUR par défaut, une autre stockait le montant sous forme de chaîne plutôt que de nombre à virgule flottante après un export CSV mal nettoyé. Un bug de facturation a fini par additionner un montant en euros avec un montant censé être en dollars, sans qu’aucune vérification n’ait jamais empêché ce mélange.
Ce type de bug, difficile à repérer à la revue de code tant le tableau associatif semble anodin, illustre précisément où un value object apporte une garantie qu’un tableau ne peut structurellement pas offrir : la certitude que toute instance manipulée respecte une forme et un type précis, vérifiés une seule fois, à sa création.
Ce qu’un tableau associatif ne garantit jamais
Un tableau PHP, aussi structuré soit-il par convention, reste une structure de données générique. Rien n’empêche une fonction d’oublier une clé, d’en ajouter une supplémentaire non prévue, ou de stocker un type différent de celui attendu par le reste du code. Ces erreurs ne se révèlent qu’à l’exécution, souvent loin de l’endroit où le tableau a été mal construit, ce qui rend le diagnostic long et frustrant.
function calculer_total( array $montants ) {
$total = 0;
foreach ( $montants as $montant ) {
$total += $montant['montant']; // suppose que la clé existe, sans vérifier
}
return $total;
}
// Quelque part ailleurs dans le code, produit par une autre fonction :
$montants[] = array( 'valeur' => 42.5, 'devise' => 'EUR' ); // clé "valeur" au lieu de "montant"
Ce genre d’erreur de nommage, aussi simple soit-elle, ne produit ni erreur PHP ni avertissement par défaut : elle se traduit silencieusement par un total incorrect, potentiellement pendant des semaines avant qu’une anomalie de facturation ne soit repérée manuellement.
Le value object comme garde-fou
Un value object encapsule ces mêmes données, mais impose leur validation dès la construction de l’objet, et interdit toute modification ultérieure de ses propriétés une fois créé, d’où le terme « immuable » souvent associé à ce patron de conception :

final class Montant {
private float $valeur;
private string $devise;
public function __construct( float $valeur, string $devise ) {
if ( $valeur < 0 ) {
throw new InvalidArgumentException( 'Un montant ne peut pas être négatif.' );
}
if ( ! in_array( $devise, array( 'EUR', 'USD', 'GBP' ), true ) ) {
throw new InvalidArgumentException( "Devise non gérée : $devise" );
}
$this->valeur = $valeur;
$this->devise = $devise;
}
public function valeur() : float {
return $this->valeur;
}
public function devise() : string {
return $this->devise;
}
public function additionner( Montant $autre ) : self {
if ( $this->devise !== $autre->devise ) {
throw new InvalidArgumentException( 'Impossible d\'additionner deux devises différentes.' );
}
return new self( $this->valeur + $autre->valeur, $this->devise );
}
}
Le bug de facturation évoqué en introduction devient tout simplement impossible avec cette structure : la méthode additionner() refuse explicitement de mélanger deux devises différentes, avec une exception levée immédiatement à l’endroit précis où l’erreur se produit, plutôt qu’un total silencieusement incohérent découvert bien plus tard.
La comparaison d’égalité, un autre piège des tableaux
Comparer deux tableaux associatifs avec l’opérateur == fonctionne tant que leurs clés sont dans le même ordre et que leurs types correspondent exactement, mais devient rapidement source de surprises dès que l’un des deux tableaux a été construit différemment, même avec un contenu logiquement identique. Un value object, à l’inverse, peut définir explicitement sa propre méthode d’égalité, indépendante de tout ordre interne :
public function egal( Montant $autre ) : bool {
return $this->valeur === $autre->valeur() && $this->devise === $autre->devise();
}
Où le tableau associatif reste parfaitement adapté
Ce constat ne condamne pas l’usage des tableaux associatifs en général. Pour une structure de données transitoire, utilisée localement dans une seule fonction sans jamais être transmise entre plusieurs couches de l’extension, un tableau reste parfaitement suffisant et plus rapide à écrire. Le passage au value object se justifie précisément quand une structure de données traverse plusieurs fonctions ou modules différents, chacun susceptible de la produire ou de la consommer, et quand une valeur invalide aurait un coût métier réel si elle passait inaperçue.
- Donnée locale à une seule fonction, jamais partagée : le tableau associatif reste adapté, sans complexité inutile.
- Donnée métier centrale, partagée entre plusieurs modules d’une extension : le value object apporte une garantie de validité qu’aucun tableau ne peut offrir structurellement.
- Ne pas confondre value object et entité : un value object n’a pas d’identité propre, deux instances aux mêmes valeurs sont interchangeables, contrairement à une entité identifiée par un identifiant unique.
Un principe qui guide désormais nos choix d’architecture sur ce point précis : dès qu’une structure de données représente une notion métier centrale d’une extension, comme un montant, une adresse ou une plage de dates, la question n’est plus de savoir si elle mérite un value object, mais depuis combien de bugs silencieux elle en aurait déjà eu besoin.
En résumé
Un tableau associatif ne garantit rien sur sa propre forme, ni sur la présence de ses clés, ni sur le type de ses valeurs, ce qui en fait un choix risqué dès qu’une donnée métier centrale circule entre plusieurs parties d’une extension. Un value object déplace cette validation à un seul endroit, sa construction, et rend structurellement impossibles des erreurs qu’un tableau laisse toujours filtrer silencieusement jusqu’à ce qu’un incident les révèle.