← Retour aux articles
Architecture PHP et patrons de conception

DTO vs Value Object : démêler la confusion en PHP

Un guide clair sur la différence entre les DTO et les Value Objects en PHP : leur objectif, comportement, validation et quand utiliser chacun dans une architecture propre.

✦
Article en vedette ↗

DTO vs Value Object : démêler la confusion en PHP

Si vous avez passé un peu de temps dans les cercles PHP modernes, vous avez entendu les deux termes : DTO et Value Object. Ils sonnent de manière similaire, ce sont tous deux des classes simples, et tous deux contiennent des données. Les développeurs les utilisent souvent de manière interchangeable — et c'est une erreur qui mène à une architecture confuse.

Dans cet article, nous allons démêler la confusion une fois pour toutes. Vous apprendrez à quoi sert chaque pattern, en quoi ils diffèrent et — surtout — comment décider lequel vous avez réellement besoin dans une situation donnée.

La racine de la confusion

Les DTO et les Value Objects partagent plusieurs caractéristiques qui les rendent faciles à confondre :

  • Ce sont tous deux de petites classes.
  • Tous deux contiennent des données.
  • Ils sont souvent immuables.
  • Dans des cas simples, ils peuvent sembler presque identiques.

Mais ils existent pour des raisons totalement différentes. Les confondre, c'est comme confondre une boîte d'expédition avec une pièce de monnaie — ce sont tous deux des objets, mais leur but, leurs règles et leur cycle de vie sont complètement différents.

Qu'est-ce qu'un DTO ?

Un Data Transfer Object est une classe dont le rôle est de transporter des données entre les couches ou les processus. C'est tout. Il n'a aucun comportement au-delà de l'obtention et de la définition de valeurs (ou de l'exposition de propriétés readonly).

Cas d'utilisation typiques :

  • Retourner des données d'un contrôleur vers une vue.
  • Passer des entrées d'une requête HTTP à un service.
  • Sérialiser des données en JSON pour une réponse API.
  • Mapper des lignes de base de données vers une structure adaptée à l'application.

Un DTO simple ressemble à ceci :

<?php declare(strict_types=1); final class CreateUserDTO { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } }

Remarquez ce qui manque : pas de validation, pas de règles métier, pas de signification de domaine. C'est un conteneur. Rien de plus.

Qu'est-ce qu'un Value Object ?

Un Value Object est une classe qui représente un concept de votre domaine. Il est entièrement défini par ses valeurs, n'a pas d'identité et — surtout — il impose sa propre validité et encapsule le comportement lié à ces valeurs.

Cas d'utilisation typiques :

  • Représenter un Email, Money, UserId, PhoneNumber ou DateRange.
  • Garantir qu'une valeur est toujours valide partout où elle est utilisée.
  • Encapsuler des règles métier comme la correspondance des devises ou les chevauchements de dates.
  • Éliminer l'obsession des primitives dans votre modèle de domaine.

Un Value Object simple ressemble à ceci :

<?php declare(strict_types=1); final class Email { private function __construct( private readonly string $value, ) { } public static function fromString(string $value): self { $normalized = mb_strtolower(trim($value)); if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException( sprintf('"%s" is not a valid email address.', $value) ); } return new self($normalized); } public function toString(): string { return $this->value; } public function equals(self $other): bool { return $this->value === $other->value; } }

Remarquez ce qui est présent ici : validation, normalisation, égalité et intention. L'objet ne fait pas que contenir des données — il est un concept.

Comparaison côte à côte

  • Objectif : DTO — transporter des données ; VO — représenter un concept du domaine.
  • Comportement : DTO — aucun (uniquement getters/setters ou props readonly) ; VO — peut contenir un comportement du domaine.
  • Validation : DTO — validé en externe (par exemple, dans une form request) ; VO — se valide lui-même à la création.
  • Identité : DTO — non pertinente ; VO — défini par les valeurs, pas d'identité.
  • Égalité : DTO — généralement non implémentée ; VO — égalité basée sur les valeurs, souvent via equals().
  • Immuabilité : DTO — optionnelle ; VO — obligatoire.
  • Couche : DTO — couche applicative (frontières) ; VO — couche domaine.
  • Source de vérité : DTO — données externes ; VO — règles du domaine.

Un exemple concret

Imaginez que vous construisez une fonctionnalité d'inscription utilisateur. Vous recevez une requête HTTP avec des données brutes. C'est là qu'un DTO brille :

<?php final class RegisterUserRequest { public function __construct( public readonly string $name, public readonly string $email, public readonly int $age, ) { } public static function fromArray(array $data): self { return new self( name: $data['name'], email: $data['email'], age: (int) $data['age'], ); } }

Une fois la validation passée, vous convertissez le DTO en objet du domaine qui utilise des Value Objects :

<?php final class User { public function __construct( public readonly UserId $id, public readonly Name $name, public readonly Email $email, public readonly Age $age, ) { } } $user = new User( id: UserId::generate(), name: Name::fromString($dto->name), email: Email::fromString($dto->email), age: Age::fromInt($dto->age), );

Vous voyez le flux ? Le DTO a transporté des données brutes à travers la frontière de l'application. Les Value Objects ont protégé le domaine contre un état invalide. Chaque pattern a fait son travail — et aucun ne pouvait remplacer l'autre.

Où les gens se trompent

La confusion entre DTO et VO mène généralement à l'une des deux erreurs suivantes :

  1. Utiliser un DTO comme Value Object. Vous vous retrouvez avec une logique de validation éparpillée dans les services parce que le DTO ne valide pas. Le domaine reste non protégé.
  2. Utiliser un Value Object comme DTO. Vous forcez des concepts du domaine à assumer des préoccupations de transport — en ajoutant des méthodes de sérialisation, « toArray() » ou des constructeurs supplémentaires — polluant votre domaine avec de la logique d'infrastructure.

Les deux erreurs érodent les frontières dont dépend une architecture propre.

Comment décider : une heuristique simple

Demandez-vous : cette classe représente-t-elle un concept avec des règles, ou transporte-t-elle simplement des données ?

  • Si elle représente un concept du domaine (email, money, order id) → Value Object.
  • Si elle transporte des données entre les couches (entrée de formulaire, payload API, résultat de requête) → DTO.

Une autre façon d'y penser :

  • Un DTO est une forme — il définit quels champs existent.
  • Un Value Object est une signification — il définit ce que la valeur est et ce qu'elle peut faire.

Un DTO peut-il contenir des Value Objects ?

Oui, et c'est souvent une bonne idée. Un DTO peut transporter des Value Objects au lieu de primitives lorsque la frontière l'exige :

<?php final class UpdateUserProfileDTO { public function __construct( public readonly UserId $userId, public readonly ?Name $name, public readonly ?Email $email, ) { } }

C'est acceptable tant que le DTO n'ajoute pas de comportement et que les Value Objects continuent d'imposer leurs règles. Le DTO transporte toujours ; les VO protègent toujours.

Un Value Object peut-il être utilisé directement dans la sérialisation ?

Dans de nombreuses applications, les Value Objects sont sérialisés en JSON via des normalisateurs personnalisés ou en exposant une méthode toString() ou toNative(). C'est acceptable — mais évitez de mélanger les préoccupations de persistance ou de transport avec les responsabilités principales du VO. Si la logique de sérialisation devient lourde, placez-la dans un sérialiseur séparé plutôt qu'à l'intérieur du VO lui-même.

Meilleures pratiques

  • Gardez les DTO ennuyeux. Pas de validation, pas de règles métier, pas de magie cachée. Juste des données et des constructeurs.
  • Gardez les Value Objects stricts. Validez à la création, imposez les invariants, fournissez un comportement utile.
  • Ne brouillez pas les frontières. Convertissez les DTO en objets du domaine à la frontière de votre application.
  • Choisissez l'immuabilité partout. Les deux patterns bénéficient de readonly et de la promotion de constructeur en PHP 8.
  • Laissez le domaine parler. Partout où une primitive pourrait porter une signification, remplacez-la par un Value Object.

Conclusion

Les DTO et les Value Objects sont tous deux simples, puissants et souvent confondus. La distinction n'est pas académique — elle est architecturale. Un DTO est un coursier ; un Value Object est un citoyen de votre domaine. L'un déplace des données, l'autre impose une signification.

Utilisez les DTO aux frontières de votre application pour transporter des données vers l'intérieur et l'extérieur. Utilisez les Value Objects à l'intérieur de votre domaine pour rendre les états invalides impossibles et exprimer clairement les règles métier. Lorsque vous gardez ces rôles séparés, votre base de code devient plus facile à tester, plus facile à raisonner et beaucoup plus difficile à casser.

Ne les traitez pas comme interchangeables. Traitez-les comme complémentaires — et votre architecture vous remerciera.

Technologies et sujets

Tags de l'article

Aucun article ne correspond à ces filtres.

Vous avez un projet ou une idée à discuter ?

Parlons-en ↗