← Torna agli articoli
Architettura PHP e pattern di progettazione

DTO vs Value Object: districare la confusione in PHP

Una guida chiara alla differenza tra DTO e Value Objects in PHP: il loro scopo, comportamento, validazione e quando usare ciascuno in un'architettura pulita.

✦
Articolo in evidenza ↗

DTO vs Value Object: districare la confusione in PHP

Se hai passato un po' di tempo nei circoli PHP moderni, hai sentito entrambi i termini: DTO e Value Object. Suonano simili, sono entrambe classi semplici e entrambe contengono dati. Gli sviluppatori spesso li usano in modo intercambiabile — e questo è un errore che porta a un'architettura confusa.

In questo articolo districheremo la confusione una volta per tutte. Imparerai a cosa serve ciascun pattern, in cosa differiscono e — cosa più importante — come decidere quale ti serve davvero in una determinata situazione.

La radice della confusione

Sia i DTO che i Value Objects condividono diverse caratteristiche che li rendono facilmente confondibili:

  • Sono entrambe classi piccole.
  • Entrambe contengono dati.
  • Sono spesso immutabili.
  • In casi semplici possono sembrare quasi identici.

Ma esistono per ragioni completamente diverse. Confonderli è come confondere una scatola da spedizione con una moneta — entrambi sono oggetti, ma il loro scopo, le loro regole e il loro ciclo di vita sono completamente diversi.

Cos'è un DTO?

Un Data Transfer Object è una classe il cui compito è trasportare dati tra livelli o processi. Tutto qui. Non ha comportamento oltre a ottenere e impostare valori (o esporre proprietà readonly).

Casi d'uso tipici:

  • Restituire dati da un controller a una view.
  • Passare input da una richiesta HTTP a un servizio.
  • Serializzare dati in JSON per una risposta API.
  • Mappare righe del database in una struttura adatta all'applicazione.

Un DTO semplice appare così:

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

Nota cosa manca: nessuna validazione, nessuna regola di business, nessun significato di dominio. È un contenitore. Niente di più.

Cos'è un Value Object?

Un Value Object è una classe che rappresenta un concetto del tuo dominio. È definito interamente dai suoi valori, non ha identità e — cosa cruciale — impone la propria validità e incapsula il comportamento relativo a quei valori.

Casi d'uso tipici:

  • Rappresentare Email, Money, UserId, PhoneNumber o DateRange.
  • Garantire che un valore sia sempre valido ovunque venga utilizzato.
  • Incapsulare regole di dominio come la corrispondenza delle valute o le sovrapposizioni di date.
  • Eliminare l'ossessione per le primitive nel tuo modello di dominio.

Un Value Object semplice appare così:

<?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; } }

Nota cosa è presente qui: validazione, normalizzazione, uguaglianza e intento. L'oggetto non si limita a contenere dati — è un concetto.

Confronto fianco a fianco

  • Scopo: DTO — trasportare dati; VO — rappresentare un concetto del dominio.
  • Comportamento: DTO — nessuno (solo getter/setter o props readonly); VO — può contenere comportamento di dominio.
  • Validazione: DTO — validato esternamente (ad esempio, in una form request); VO — si autovalida alla creazione.
  • Identità: DTO — irrilevante; VO — definito dai valori, nessuna identità.
  • Uguaglianza: DTO — di solito non implementata; VO — uguaglianza basata sui valori, spesso tramite equals().
  • Immutabilità: DTO — opzionale; VO — obbligatoria.
  • Livello: DTO — livello applicativo (confini); VO — livello di dominio.
  • Fonte di verità: DTO — dati esterni; VO — regole di dominio.

Un esempio concreto

Immagina di costruire una funzionalità di registrazione utente. Ricevi una richiesta HTTP con dati grezzi. È qui che un DTO brilla:

<?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'], ); } }

Una volta superata la validazione, converti il DTO in un oggetto di dominio che utilizza 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), );

Vedi il flusso? Il DTO ha trasportato dati grezzi attraverso il confine dell'applicazione. I Value Objects hanno protetto il dominio da stati non validi. Ogni pattern ha fatto il suo lavoro — e nessuno poteva sostituire l'altro.

Dove le persone sbagliano

La confusione tra DTO e VO porta tipicamente a uno dei due errori:

  1. Usare un DTO come Value Object. Ti ritrovi con logica di validazione sparsa nei servizi perché il DTO non valida. Il dominio rimane non protetto.
  2. Usare un Value Object come DTO. Costringi concetti del dominio ad assumere preoccupazioni di trasporto — aggiungendo metodi di serializzazione, «toArray()» o costruttori extra — inquinando il tuo dominio con logica infrastrutturale.

Entrambi gli errori erodono i confini da cui dipende l'architettura pulita.

Come decidere: un'euristica semplice

Chiediti: questa classe rappresenta un concetto con regole, o trasporta semplicemente dati?

  • Se rappresenta un concetto del dominio (email, money, order id) → Value Object.
  • Se trasporta dati tra livelli (input di form, payload API, risultato di query) → DTO.

Un altro modo di pensarci:

  • Un DTO è una forma — definisce quali campi esistono.
  • Un Value Object è un significato — definisce cosa il valore è e cosa può fare.

Un DTO può contenere Value Objects?

Sì, e spesso è una buona idea. Un DTO può trasportare Value Objects invece di primitive quando il confine lo richiede:

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

Va bene fintanto che il DTO non aggiunge comportamento e i Value Objects continuano a imporre le loro regole. Il DTO trasporta ancora; i VO proteggono ancora.

Un Value Object può essere usato direttamente nella serializzazione?

In molte applicazioni, i Value Objects vengono serializzati in JSON tramite normalizzatori personalizzati o esponendo un metodo toString() o toNative(). Questo è accettabile — ma evita di mescolare preoccupazioni di persistenza o trasporto con le responsabilità principali del VO. Se la logica di serializzazione diventa pesante, mettila in un serializzatore separato piuttosto che dentro il VO stesso.

Migliori pratiche

  • Mantieni i DTO noiosi. Nessuna validazione, nessuna regola di business, nessuna magia nascosta. Solo dati e costruttori.
  • Mantieni i Value Objects rigorosi. Valida alla creazione, imponi invarianti, fornisci comportamento utile.
  • Non confondere i confini. Converti i DTO in oggetti di dominio al bordo della tua applicazione.
  • Scegli l'immutabilità ovunque. Entrambi i pattern traggono beneficio da readonly e dalla promozione del costruttore in PHP 8.
  • Lascia parlare il dominio. Ovunque una primitiva possa portare significato, sostituiscila con un Value Object.

Conclusione

DTO e Value Objects sono entrambi semplici, potenti e spesso confusi. La distinzione non è accademica — è architetturale. Un DTO è un corriere; un Value Object è un cittadino del tuo dominio. Uno sposta dati, l'altro impone significato.

Usa i DTO ai confini della tua applicazione per trasportare dati dentro e fuori. Usa i Value Objects all'interno del tuo dominio per rendere impossibili gli stati non validi ed esprimere chiaramente le regole di business. Quando mantieni questi ruoli separati, la tua codebase diventa più facile da testare, più facile da ragionare e molto più difficile da rompere.

Non trattarli come intercambiabili. Trattali come complementari — e la tua architettura ti ringrazierà.

Tecnologie e argomenti

Tag dell'articolo

Nessun articolo corrisponde a questi filtri.

Hai un progetto o un'idea da discutere?

Parliamone ↗