Cast un attribut de modèle en DTO

Publié le 29 août 2023 par William Suppo
Couverture de l'article Cast un attribut de modèle en DTO

Utilisons un DTO en tant qu’attribut afin de rendre notre code plus consistant et d'éviter les surprises !

Implémentation du DTO

Nous allons partir sur un cas d’usage simple : un modèle Post qui possède les attributs suivants :

Notre DTO Image possède, quant à lui, les propriétés src et alt , voici son implémentation :

1<?php
2 
3namespace App\DataTransfertObjects;
4 
5class Image
6{
7 public function __construct(
8 public readonly string $src,
9 public readonly string $alt,
10 ) {
11 }
12}

Création du modèle

Commençons par implémenter la migration de notre modèle :

1<?php
2 
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6 
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('posts', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->string('title');
18 $table->text('content');
19 $table->json('image');
20 });
21 }
22 
23 /**
24 * Reverse the migrations.
25 */
26 public function down(): void
27 {
28 Schema::dropIfExists('posts');
29 }
30};

À noter que la colonne image est de type json, ce qui nous permettra de sérialiser notre DTO en base de données.

Tant que nous abordons les données, implémentons la Factory de notre modèle afin de pouvoir nourrir facilement notre base :

1<?php
2 
3namespace Database\Factories;
4 
5use App\DataTransfertObjects\Image;
6use Illuminate\Database\Eloquent\Factories\Factory;
7 
8/**
9 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
10 */
11class PostFactory extends Factory
12{
13 /**
14 * Define the model's default state.
15 *
16 * @return array<string, mixed>
17 */
18 public function definition(): array
19 {
20 return [
21 'title' => 'Ceci est un titre ' . $this->faker->randomDigit(),
22 'content' => $this->faker->paragraph(),
23 'image' => new Image(
24 src: $this->faker->imageUrl(),
25 alt: $this->faker->sentence(),
26 ),
27 ];
28 }
29}

La particularité ici réside dans l’instanciation d’un DTO afin de construire l’attribut image de notre modèle.

Enfin, découvrons notre modèle Post et surtout la manière dont on ordonne à Laravel de transformer le json en objet Image :

1<?php
2 
3namespace App\Models;
4 
5use App\Casts\ImageCaster;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Database\Eloquent\Model;
8 
9class Post extends Model
10{
11 use HasFactory;
12 
13 protected $casts = [
14 'image' => ImageCaster::class,
15 ];
16}

Voilà, c’est via la classe ImageCaster que Laravel va transformer la propriété, à nous d’y définir comment cela sera fait dans le prochain chapitre.

Création du Caster

Laravel nous offre la possibilité de nous créer un caster via une commande, utilisons là afin de produire notre ImageCaster :

1php artisan make:cast ImageCaster

Nous voilà en possession d’une nouvelle classe App\Casts\ImageCaster dont il nous reste à définir les méthodes get et set.

La méthode get est appelé lorsque l’on récupère des données de la base afin de créer l’instance du modèle, autrement dit, nous allons y définir comment transformer le json en Image :

1public function get(Model $model, string $key, mixed $value, array $attributes): mixed
2{
3 $attributes = json_decode($value, true);
4 
5 return new Image(
6 src: $attributes['src'],
7 alt: $attributes['alt'],
8 );
9}

On décode le contenu du json en tableau et on crée notre instance Image.

Enfin, la méthode set, qui est l’opposée de get, va définir comment sérialiser une instance d’Image :

1public function set(Model $model, string $key, mixed $value, array $attributes): mixed
2{
3 if (! $value instanceof Image) {
4 throw new InvalidArgumentException('The given value is not an Image');
5 }
6 
7 return json_encode([
8 'src' => $value->src,
9 'alt' => $value->alt,
10 ]);
11}

On vérifie que value est bien du type attendu, puis on transforme notre Image en json.

Bonus

Nous avons dorénavant toutes les briques à notre disposition afin d’utiliser sereinement notre modèle Post dans une vue par exemple :

1<body>
2 @foreach($posts as $post)
3 <div>
4 <h2>{{ $post->title }}</h2>
5 <img src="{{ $post->image->src }}" alt="{{ $post->image->alt }}">
6 <div>{{ $post->content }}</div>
7 </div>
8 @endforeach
9</body>

Nous venons de faire un tour sur le potentiel du casting de Laravel.

Pour aller plus loin, libre à vous de consolider l’instanciation du DTO en vérifiant la bonne présence d’une clé et le cas échéant définir une valeur par défaut par exemple.

William Suppo avatar
William Suppo
Je mange du PHP au p'tit dej', et vous ?

A lire

Autres articles de la même catégorie