Les objets valeur plutôt que l'obsession primitive
Un string $email peut contenir "not-an-email" jusque dans votre base de
données. Le système de types ne ment pas - il ne dit simplement pas la
vérité dont vous avez besoin.
Mauvais - l'invariant ne vit nulle part
final class RegisterUser
{
public function __construct(
private readonly UserRepository $users,
) {}
public function handle(string $email, string $password): void
{
if (!str_contains($email, '@')) {
throw new InvalidArgumentException('Invalid email');
}
$this->users->save(new User($email, $password));
}
}
Le contrôle est correct aujourd'hui. Il doit aussi être mémorisé, et réimplémenté, partout où une adresse email entre dans le système - un second cas d'usage, un script d'import admin, une commande console. En oublier un et l'invariant disparaît.
Bon - l'invariant est le type
final class EmailAddress
{
private function __construct(private readonly string $value) {}
public static function fromString(string $value): self
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailAddress($value);
}
return new self($value);
}
public function __toString(): string
{
return $this->value;
}
}
final class RegisterUser
{
public function __construct(
private readonly UserRepository $users,
) {}
public function handle(EmailAddress $email, Password $password): void
{
$this->users->save(new User($email, $password));
}
}
EmailAddress ne peut pas exister dans un état invalide - la construction
est la validation. Chaque point d'appel en aval, y compris ceux écrits dans
six mois par quelqu'un qui n'a pas lu ce fichier, obtient la garantie
gratuitement.
Le compromis : plus de classes, plus de cérémonie pour ce qui ressemble à une chaîne de caractères. Ça vaut le coup exactement là où l'invariant est critique pour le métier, pas pour chaque champ de chaque formulaire.