Les Value Objects en PHP : arrêtez d'écrire du code obsédé par les primitives
Regardez presque n'importe quelle base de code PHP et vous trouverez le même schéma : les e-mails stockés sous forme de chaînes, l'argent stocké sous forme de floats, les ID stockés sous forme d'entiers, les numéros de téléphone stockés sous forme de chaînes. C'est ce qu'on appelle la primitive obsession (obsession des primitives) — l'utilisation excessive de types primitifs pour représenter des concepts du domaine. C'est l'un des anti-patterns les plus courants et les plus dommageables du développement PHP moderne.
Les Value Objects sont le remède. Ils apportent sécurité des types, validation et clarté à votre code — et une fois que vous commencerez à les utiliser, vous vous demanderez comment vous avez pu vivre sans eux.
Qu'est-ce qu'un Value Object ?
Un Value Object (VO, objet-valeur) est un petit objet immuable qui représente un concept de votre domaine. Contrairement à une Entity, un Value Object n'a pas d'identité — il est entièrement défini par ses valeurs. Deux Value Objects avec les mêmes valeurs sont considérés comme égaux.
Les exemples classiques incluent :
- Email — au lieu d'une chaîne brute
- Money — au lieu d'un float plus une chaîne de devise
- UserId — au lieu d'un entier
- PhoneNumber — au lieu d'une chaîne
- DateRange — au lieu de deux objets DateTime
- Address — au lieu d'un tableau de chaînes
- Password — au lieu d'une simple chaîne
Le problème avec les primitives
Considérez ce code typique :
Chaque valeur primitive porte des hypothèses cachées. Le système de types ne vous dit rien sur :
- Si la valeur est valide
- Dans quel format elle se trouve
- Quelles opérations sont autorisées
- Quelles unités ou devises elle représente
- Si elle peut être modifiée ou réutilisée en toute sécurité
Le résultat ? Une logique de validation éparpillée partout, des bugs dus à des formats incompatibles et un code difficile à raisonner.
Votre premier Value Object : Email
Remplaçons une chaîne primitive par un véritable Value Object :
Maintenant, le système de types vous protège. Si vous avez un objet Email, vous savez qu'il est valide. Plus de vérifications de validation éparpillées.
Utilisation du Value Object
Un exemple plus complexe : Money
L'argent est le cas classique où les primitives échouent catastrophiquement. Les floats perdent de la précision, et mélanger les devises est un bug silencieux qui n'attend que de se produire.
Maintenant, regardez à quel point le code appelant devient propre :
Caractéristiques clés d'un bon Value Object
- Immuable — une fois créé, il ne change jamais. Les opérations retournent de nouvelles instances.
- Auto-validant — une instance ne peut pas exister dans un état invalide.
- Sans identité — l'égalité est basée sur les valeurs, pas sur un ID de base de données.
- Sans effet de bord — aucun appel à la base de données, aucune journalisation, aucun état global.
- Petit — représente un concept, pas un agrégat entier.
- Remplaçable — l'objet entier peut être échangé, plutôt que muté champ par champ.
Value Object vs. DTO vs. Entity
Il est important de distinguer ces trois patterns :
- Value Object — immuable, défini par ses valeurs, contient un comportement lié à ces valeurs (comme
add()sur Money). - DTO — immuable ou mutable, transporte des données entre les couches, ne contient aucune logique métier.
- Entity — possède une identité unique qui persiste dans le temps, même lorsque ses attributs changent.
Là où les Value Objects brillent
- Modèles de domaine — représentation d'e-mails, d'argent, d'ID, de dates, d'adresses.
- Validation des entrées — créer un VO à partir des entrées utilisateur garantit la validité partout en aval.
- Règles métier — encapsulation de logique telle que la correspondance des devises ou la vérification des plages de dates.
- Tests — des tests plus simples, plus rapides et plus ciblés sans dépendances à la base de données.
- Refactoring — lorsque les exigences changent, vous modifiez le VO à un seul endroit.
Pièges courants à éviter
- Ajouter des setters — cela brise l'immuabilité. Si vous avez besoin d'une valeur différente, créez une nouvelle instance.
- Les rendre trop gros — un VO doit représenter un concept, pas une racine d'agrégat entière.
- Ajouter une logique de persistance — gardez les préoccupations de base de données dans les repositories, pas dans les VOs.
- Ignorer l'égalité — implémentez toujours une méthode
equals(). - Lancer des exceptions génériques — définissez des exceptions spécifiques au domaine pour une gestion d'erreurs plus claire.
Un chemin de refactoring pratique
Vous n'avez pas besoin de réécrire toute votre application du jour au lendemain. Commencez ici :
- Identifiez un champ primitif qui cause des bugs ou de la confusion (e-mails, argent, ID).
- Créez un Value Object pour celui-ci avec validation et méthodes utiles.
- Remplacez la primitive dans une classe ou un module.
- Laissez le compilateur et vos tests guider le reste.
- Répétez avec le prochain candidat évident.
Conclusion
Les Value Objects sont l'un des refactorings les plus efficaces que vous puissiez effectuer dans une base de code PHP. Ils éliminent l'obsession des primitives, centralisent la validation, rendent les états invalides impossibles à représenter et transforment votre code en une expression auto-documentée de votre domaine. Le système de types devient un allié plutôt qu'une formalité.
Commencez par un seul Value Object — peut-être Email ou Money. Une fois que vous verrez à quel point cela apporte de la clarté à votre code, vous ne reviendrez jamais à faire passer des chaînes brutes.
Arrêtez d'écrire du code obsédé par les primitives. Laissez vos types parler pour votre domaine.