Object Mapper : l’art de la simplicité

Temps de lecture : 7 min

Comment le composant Object Mapper de Symfony 7.3 simplifie la transformation d’entités en DTOs. Fini le code répétitif : mappez vos objets de façon déclarative, exploitez la puissance des Transformers, et gardez un code propre et lisible.

J’ai décidé de reprendre mes DTOs en utilisant le composant Object Mapper introduit avec Symfony 7.3 pour appliquer à la lettre ces mots de Léonard de Vinci « La simplicité est la sophistication supprême ». Si vous en avez marre d'écrire du code répétitif pour transformer vos entités en DTOs, ce composant va devenir votre nouveau meilleur ami

Qu'est-ce que Object Mapper ?

Imaginez que vous avez une entité complexe avec plein de relations, de propriétés et de méthodes. Maintenant, vous voulez l'afficher dans votre interface utilisateur, mais vous n'avez besoin que de certaines propriétés, peut-être transformées d'une certaine façon. C'est là qu'intervient Object Mapper !

Object Mapper est un nouveau composant Symfony qui permet de mapper (ou transformer) des objets d'une classe vers une autre, de manière déclarative et élégante. Fini les méthodes toArray() interminables ou les constructeurs avec 15 paramètres !

Installation et configuration

Avant de plonger dans les exemples concrets, assurons-nous que vous avez tout ce qu'il faut pour suivre. L'installation est simple avec Composer :

composer require symfony/object-mapper

Et voilà, vous êtes prêt à mapper !

Les bases du mapping

Le principe fondamental d’Object Mapper est d'utiliser des attributs PHP pour déclarer comment les propriétés doivent être mappées d'un objet à un autre. Prenons un exemple concret tiré de mon code :

#[Map(target: Post::class)]
class PostDto
{
    #[Map(target: 'id')]
    public ?int $id = null;

    #[Map(target: 'title')]
    public string $title;
    
    // ... autres propriétés
}

Dans cet exemple, nous déclarons que notre PostDto peut être mappé vers une entité Post. L'attribut #[Map] au niveau de la classe définit la classe cible par défaut, tandis que les attributs #[Map] sur les propriétés indiquent comment chaque propriété doit être mappée.

Utilisation basique

Maintenant, voyons comment utiliser concrètement l'Object Mapper dans notre code. Voici un exemple tiré de notre composant Twig Post :

final class Post
{
    public ?PostEntity $post = null;

    public function __construct(
        private readonly ObjectMapperInterface $objectMapper,
    ) {
    }

    #[ExposeInTemplate('card')]
    public function getPostCard(): ?PostDto
    {
        return $this->objectMapper->map($this->post, PostDto::class);
    }
}

C'est incroyablement simple ! Une seule ligne de code ($this->objectMapper->map($this->post, PostDto::class)) suffit pour transformer notre entité Post en DTO. Object Mapper s'occupe de tout le travail fastidieux.

La transformation de valeurs : la puissance cachée

Jusqu'ici, nous avons vu des mappings simples, mais la vraie puissance de Object Mapper réside dans sa capacité à transformer les valeurs pendant le mapping. C'est là que les choses deviennent vraiment intéressantes !

Les Transformers

Les transformers sont des classes qui implémentent l'interface TransformCallableInterface et qui permettent de modifier une valeur pendant le processus de mapping. Regardons un exemple concret avec notre CategoryColorTransformer :

class CategoryColorTransformer implements TransformCallableInterface
{
    public function __invoke(mixed $value, object $source, ?object $target): string
    {
        return $value?->getColor()->value ?? 'blue';
    }
}

Ce transformer est très simple : il prend un objet catégorie, extrait sa couleur, et retourne la valeur sous forme de chaîne. Si la catégorie est null ou n'a pas de couleur, il retourne blue par défaut.

Utilisation des Transformers dans le DTO

Maintenant, voyons comment utiliser ce transformer dans notre DTO :

#[Map(source: 'category', transform: CategoryColorTransformer::class)]
public string $categoryColor;

Ici, nous indiquons que la propriété $categoryColor doit être remplie en prenant la propriété category de l'objet source, puis en la passant à travers notre CategoryColorTransformer.

Examinons d'autres exemples de transformations dans notre PostDto :

#[Map(source: 'category', transform: CategoryTitleTransformer::class)]
public string $categoryTitle;

#[Map(source: 'category', transform: CategorySlugTransformer::class)]
public string $categorySlug;

#[Map(source: 'seo', transform: ExcerptTransformer::class)]
public string $excerpt;

Chacune de ces propriétés utilise un transformer différent pour extraire et formater une information spécifique à partir d'un objet complexe.

Analyse Détaillée de Notre Exemple

Maintenant que nous avons couvert les bases, analysons en détail comment l'Object Mapper est utilisé dans notre projet.

Le DTO (PostDto.php)

Notre PostDto est un excellent exemple d'utilisation de Object Mapper :

#[Map(target: Post::class)]
class PostDto
{
    #[Map(target: 'id')]
    public ?int $id = null;

    #[Map(target: 'title')]
    public string $title;

    #[Map(target: 'content')]
    public string $content;

    #[Map(target: 'slug')]
    public string $slug;

    #[Map(target: 'status')]
    public ContentStatusEnum $status;

    #[Map(source: 'category', transform: CategoryTitleTransformer::class)]
    public string $categoryTitle;

    #[Map(source: 'category', transform: CategorySlugTransformer::class)]
    public string $categorySlug;

    #[Map(source: 'category', transform: CategoryColorTransformer::class)]
    public string $categoryColor;

    #[Map(target: 'viewCount')]
    public int $viewCount;

    #[Map(target: 'createdAt')]
    public \DateTimeImmutable $createdAt;

    #[Map(target: 'updatedAt')]
    public \DateTimeImmutable $updatedAt;

    #[Map(source: 'seo', transform: ExcerptTransformer::class)]
    public string $excerpt;
}

Ce DTO combine plusieurs types de mappings : - Mappings directs (id, title, content, etc.) - Mappings avec transformation (categoryTitle, categorySlug, categoryColor, excerpt) - Mappings de types complexes (status est un enum, createdAt et updatedAt sont des objets DateTimeImmutable)

Le Transformer (CategoryColorTransformer.php)

Notre CategoryColorTransformer est un exemple parfait de transformer simple mais efficace :

class CategoryColorTransformer implements TransformCallableInterface
{
    public function __invoke(mixed $value, object $source, ?object $target): string
    {
        return $value?->getColor()->value ?? 'blue';
    }
}

Ce transformer :

L'Utilisation (Post.php)

Enfin, notre composant Twig Post montre comment utiliser Object Mapper dans un contexte réel :

final class Post
{
    public ?PostEntity $post = null;

    public function __construct(
        private readonly ObjectMapperInterface $objectMapper,
    ) {
    }

    #[ExposeInTemplate('card')]
    public function getPostCard(): ?PostDto
    {
        return $this->objectMapper->map($this->post, PostDto::class);
    }
}

Ce composant :

Cette approche permet une séparation claire entre les données de l'entité (qui peuvent contenir des informations sensibles ou inutiles pour l'affichage) et les données nécessaires à l'affichage.

Avantages de Object Mapper

L'utilisation de l'Object Mapper présente de nombreux avantages :

Le mot de la fin

Object Mapper de Symfony est un outil puissant qui peut considérablement simplifier votre code et améliorer sa maintenabilité. En utilisant des attributs déclaratifs et des transformers personnalisés, vous pouvez facilement convertir des objets complexes en structures plus simples adaptées à vos besoins spécifiques.

Nous avons vu comment Object Mapper nous permet de transformer des entités Post complexes en DTOs simples, en extrayant et en transformant les données nécessaires pour l'affichage. Cette approche nous permet de garder notre code propre, maintenable et focalisé sur sa responsabilité première.

Alors, la prochaine fois que vous vous retrouvez à écrire une méthode toArray() ou un constructeur avec une douzaine de paramètres, pensez à Object Mapper. Votre futur vous remerciera !

N'hésitez pas à explorer davantage la documentation officielle pour découvrir toutes les fonctionnalités avancées que nous n'avons pas pu couvrir dans ce billet.