Des DTO sans laravel-data

Publié le 10 mai 2023 par Mathieu De Gracia
Couverture de l'article Des DTO sans laravel-data

Au cours des dernières années l'utilisation des DTO, pour Data Transfert Object, s'est largement répandue dans l'écosystème PHP devenant un pattern pratiquement incontournable dans nos applications modernes.

Ce besoin de typage, de consistance, allant de pair avec les améliorations successives de PHP concernant les nouvelles possibilités de typage.

Nous avons traité de ce pattern à de multiples reprises (ici et ) et des package tel que Spatie/laravel-data sont désormais bien implémentés et font office de référence dès lors que vous voulez des DTO dans votre application.

Ces packages sont pour la plupart extrêmement riches en fonctionnalités (et complexe à maintenir) alors que le pattern DTO est par nature relativement simple à implementer.

Il est probable que vous n'utilisiez que partiellement ou superficiellement laravel-data ... avez-vous réellement besoin d’ajouter une dépendance externe à votre application dont vous allez exploiter qu'une infime partie de ses fonctionnalités ?

Voyons ensemble comment créer des DTO, uniquement en PHP, sans ajouter la moindre dependance en revenant à l’essence même du pattern !

Un DTO maison

Un DTO peut se résumer en une banale class permettant de transférer des données d’une couche de l’application à l’autre. Il est souvent utilisé pour encapsuler des données en un seul objet facilement manipulable et vérifiable.

Imaginons maintenant que nous souhaitions créer un DTO pour représenter un slug, nous pourrions alors créer la simple class suivante :

1class Slug {
2 public function __construct(
3 public string $value,
4 ){}
5}

Aussi basique soit-elle, cette class est un DTO, nous pourrions nous arrêtez là : elle englobe une fonctionnalité de votre métier pour le rendre manipulable.

Bien sûr quelques fonctionnalités supplémentaires ne seront pas de trop !

Contrôler l'instanciation

Tout d’abord, il est commun pour un DTO de contrôler les données qu’il s’apprête à représenter avant de s'instancier.

Plusieurs méthodes statiques répondant chacune à un cas d'utilisation spécifique seront faciles à implémenter :

1class Slug {
2 
3 private function __construct(
4 public string $value,
5 ){}
6 
7 public static function fromString(string $value): self
8 {
9 return new self($value);
10 }
11}

En modifiant la visibilité de votre constructeur en privé, vous serez contraint de passer par l'une de vos méthodes statiques, cette nouvelle contrainte vous assurera que votre objet sera initialisé correctement.

1Slug::fromString('i-am-a-slug');

Cette méthode fromstring représente le point d’entrée le plus simple et généraliste possible, cependant, si vous possédez des cas d’utilisation plus spécifiques, libre à vous de créer une nouvelle méthode statique vous permettant d’y répondre de manière adéquate.

Par exemple, ajoutons une méthode fromUser prenant comme argument une instance du model User et générant un slug depuis la propriété name de ce dernier.

1use Illuminate\Support\Str;
2 
3class Slug {
4 
5 private function __construct(
6 public string $value,
7 ){}
8 
9 public static function fromString(string $value): self
10 {
11 return new self($value);
12 }
13 
14 public static function fromUser(string $value): self
15 {
16 $slug = Str::slug($value, '-');
17 
18 return new self($slug);
19 }
20}

Le DTO détient désormais sa propre logique d'instanciation, vous pourrez ainsi décharger vos controllers, vos services, de cette responsabilité car le DTO sera capable de se créer lui-même !

Cette logique sera factorisée en un seul endroit et non plus dupliqué dans votre application grâce à ces méthodes statiques évitant ainsi la duplication de code.

Valider l'instanciation

Il est fort probable que ce soit une volonté de vérifier et valider les informations d'un DTO qui vous motiva un jour à vous rapprocher du package laravel-data.

Dans notre cas, nous souhaitons nous assurer que la valeur transmise au DTO répond à des caractéristiques précises.

1Slug::fromString('i am a slug'); // ❌
2Slug::fromString('i_am_a_slug'); // ❌
3Slug::fromString('i-am-a-slug'); // ✅

Cette validation est encore une fois relativement simple à intégrer en pur PHP, par exemple à l'aide d'une regex ;

1class Slug {
2 
3 private const VALIDATION_PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/i';
4 
5 public string $value;
6 
7 private function __construct(string $value)
8 {
9 if (! preg_match(self::VALIDATION_PATTERN, $value)) {
10 throw new Exception();
11 }
12 
13 $this->value = $value;
14 }
15 
16 public static function fromString(string $value): self
17 {
18 return new self($value);
19 }
20 
21 public static function fromUser(string $value): self
22 {
23 $slug = Str::slug($value, '-');
24 
25 return new self($slug);
26 }
27}

Désormais votre DTO est capable de s'auto-valider, vous assurant que sa valeur répond correctement aux besoins de votre application.

Cette proposition de validation, bien que plus verbeuse que les annotations de laravel-data, peut être facilement encapsulée dans un service tiers, simplifiant ainsi rapidement ce besoin métier, par exemple dans une nouvelle class Assert.

1class Slug {
2 
3 private const VALIDATION_PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/i';
4 
5 public string $value;
6 
7 private function __construct(string $value)
8 {
9 Assert::regex($value, self::VALIDATION_PATTERN);
10 
11 $this->value = $value;
12 }
13 
14 public static function fromString(string $value): self
15 {
16 return new self($value);
17 }
18 
19 public static function fromUser(string $value): self
20 {
21 $slug = Str::slug($value, '-');
22 
23 return new self($slug);
24 }
25}

Quelques fioritures

Pour finir, nous pouvons agrémenter notre DTO de quelques fioritures pour le rendre plus fonctionnelle et fun à utiliser.

En ajoutant un Readonly sur la class, vous-vous assurerez que la valeur du DTO n'a pas été modifié depuis son instanciation. C’est une très bonne pratique pour vous prévenir des effets de bord potentiels tout en étant plus en adéquation avec la philosophie du pattern DTO.

1readonly class Slug {
2 //
3}

Dans le cas présent, l'ajout d'une interface Stringable pourrait s'avérer utile pour simplifier la manipulation d'une instance de notre objet.

1class Slug implements Stringable {
2 
3 //
4 
5 public function __toString(): string
6 {
7 return $this->value;
8 }
9}

Vous pourrez ainsi l'utiliser de la manière suivante :

1$slug = Slug::fromString('i-am-a-slug');
2 
3echo $slug; // "i-am-a-slug"

Il existe de nombreuses autres interfaces en PHP qui pourraient répondre à l'un de vos besoins : Serializable, ArrayAccess ...

Conclusion

Comme vous venez de voir, le pattern DTO est relativement simple à implémenter et quelques class peuvent s'avérer parfaitement suffisantes pour commencer à l'exploiter.

Installer une nouvelle dependance externe à votre application n'est pas un choix anodin et ne devrait jamais être une solution systématique.

Une dépendance reste une contrainte que vous devrez maintenir dans le temps avec votre application, au fil des versions, tout en apportant avec elle des bugs potentiels.

Cette dépendance en question devra également être fiable et maintenue par ses créateurs, raisons de plus pour s'assurer de la pertinence d'une telle solution.

Bien évidemment, laravel-data reste une solution parfaitement pertinente, et solide, si votre besoin métier est complexe et exige des fonctionnalités précises qui dépassent le cadre de cet article.

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie