Découverte de Laravel Data : une nouvelle manière de gérer vos objets et validations
Lorsque l'on travaille sur une application web on se retrouve souvent à devoir échanger des données dans les différentes couches de notre application. Dans le cadre de Laravel nous avons 2 classes que nous allons principalement utiliser :
- Les
FormRequest
qui permettent de valider les données qui rentre dans notre application à l'aide d'un système de règles. - Les
Resources
qui permettent de normaliser les données en sortie (souvent pour une API)
Le problème
Mais ces 2 approches ont des limitations qui peuvent compliquer les choses.
Dans le cas des FormRequest
on va récupérer les données à l'aide de la méthode validated()
qui renvoie un simple tableau PHP ce qui pose plusieurs problèmes :
- Les données ne sont pas transformer par les règles de validations (une chaine de caractère satisfait la règle "int" par exemple mais reste une chaine)
- La forme du tableau doit être deviné à partie des règles de validations.
- Si on souhaite transférer les données à une autre couche de l'application on doit passer un objet représentant une requête plutôt que les données.
Pour les Resources
le problème réside dans la définition des types pour le front-end par exemple. On est obligé de maintenir à jour un type pour indiquer au front la forme de nos réponses.
C’est là qu’intervient Laravel Data, un package développé par Spatie, qui propose une approche beaucoup plus robuste en définissant des objets de données typés (DTO), capables d’être construits automatiquement depuis des tableaux ou des modèles, tout en gérant la validation, la conversion et même la génération de définitions TypeScript.
Sommaire
00:00 Présentation
02:16 Découverte
08:07 Utilisation en tant que resource
13:50 Générer les définitions TypeScript
19:29 Relations et imbrication
25:45 Conclusion
Fonctionnement
Après l'installation la librairie va permettre de créer des objets pour représenter nos données.
php artisan make:data IngredientData
Cette classe va représenter la structure de nos données et se comporte comme un simple objet PHP classique.
class IngredientData extends Data
{
public function __construct(
public string $name,
public IngredientUnit $unit,
public CarbonImmutable $created_at,
) {}
}
Utilisation pour les requêtes
Dès lors, on peut hydrater cet objet à partir de n’importe quelle source :
$data = [
'name' => 'Tomate',
'unit' => 'g',
'created_at' => '2024-01-01T12:00:00+02:00'
]
$ingredient = IngredientData::from($array);
Laravel Data va automatiquement convertir les types : les enum
, les dates (CarbonImmutable
), etc.
Mais surtout, à partir de cette définition, il est capable de générer automatiquement les règles de validation et des les utiliser automatiquement si la classe est utilisé en paramètre de notre controller
public function update(IngredientData $data) {
// $data est déjà validé et typé
}
Il est aussi possible de convertir les données sous forme de tableau avec la méthode toArray()
pour la mise à jour d'un modèle par exemple :
$model->update($data->toArray());
Utilisation en tant que resource
Mais il est aussi possible d'utiliser cet objet pour remplacer les Resources classiques de Laravel en partant d'un modèle.
$ingredient = Ingredient::first();
return IngredientData::from($ingredient);
Le package convertit le modèle en objet data, puis en JSON.
Et il permet aussi de masquer certains champs à l’export grâce à la classe Optional
:
public Optional|string $image;
Un champ optionnel n’apparaîtra pas dans la réponse s’il n’a pas de valeur associé. C’est très pratique pour éviter les null
ou gérer des champs facultatifs dans les formulaires.
On peut aussi ajouter de la logique supplémentaire si certains champs nécessite une logique supplémentaire pour être défini :
class IngredientData extends Data
{
public function __construct(
public string $name,
public IngredientUnit $unit,
public CarbonImmutable $created_at,
public string $image,
) {}
public static function fromModel(Ingredient $ingredient): self
{
return self::from(
$ingredient,
[
'image' => $ingredient->getFirstMediaUrl('image'),
]
);
}
}
Et si vous voulez ne charger un champ que si une relation est présente, la méthode lazy()
est là pour ça :
class IngredientData extends Data
{
public function __construct(
public string $name,
public IngredientUnit $unit,
public CarbonImmutable $created_at,
public Lazy|string $image,
) {}
public static function fromModel(Ingredient $ingredient): self
{
return self::from(
$ingredient,
[
'image' => Lazy::whenLoaded(
'media',
$ingredient,
fn () => $ingredient->getFirstMediaUrl('image'),
)
]
);
}
}
De cette manière, le champs ne sera chargé que si la relation a été préchargée.
Enfin, la classe Data
permet aussi de gérer les collections.
IngredientData::collect(Ingredient::paginate(3));
Générer les définitions TypeScript
Un avantage de Laravel Data est la possibilité de générer des définitions TypeScript à partir de nos objets data.
Pour cela on ajoute un attribut sur les classes que l'on souhaite voir exporter et on peut ensuite demander la génération via la commande
php artisan typescript:transform
Un fichier generated.d.ts
sera créé et on y retrouveles types correspondant à nos objets.
Il est aussi possible, au cas par cas, de spécifier le type que l'on souhaite pour une propriété spécifique.
#[LiteralTypeScriptType('File')]
public ?UploadedFile $image_file;
Ainsi, vos définitions restent parfaitement synchronisées entre le back-end et le front-end.
Relations et imbrication
Laravel Data gère aussi les relations et peut s'adapter a des structure plus complexes.
Prenons l’exemple d’une recette composée de plusieurs étapes. En ajoutant de la PHPDoc on peut indiquer le type attendu dans un tableau ou une collection d'éléments.
class RecipeData extends Data
{
public function __construct(
public string $name,
public string $description,
/** @var RecipeStepData[] */
public array $steps,
) {}
}
class RecipeStepData extends Data
{
public function __construct(
public ?int $duration,
public string $description,
) {}
}
Ainsi, le package hydrate automatiquement chaque sous-élément dans un objet RecipeStepData
et, dans le cas de la validation, validera chaque sous éléments.
Si on souhaite utiliser cet objet pour transformer un modèle on peutajouter un attribute WhenLoadedLazy
pour ne charger la relation que si elle est préchargée :
#[WhenLoaded('steps')]
public Lazy|Collection $steps;
Et, bien sûr, ces objets imbriqués peuvent aussi être exportés vers TypeScript.
Conclusion
Laravel Data apporte une approche beaucoup plus typée, claire et robuste à la gestion des données dans Laravel.
C’est un vrai plus si vous utilisez des outils d'analyse statique comme PHPStan mais aussi si travaillez avec des outils comme Inertia pour partager un typage entre le back-end et le front-end.
Mais attention à ne pas tomber dans la complexité en cherchant à réutiliser un même objet pour de trop nombreux cas d'utilisation. Les Optional
et Lazy
peuvent vite rendre vos modèles de données plus complexe que nécessaire.