Les dataTransformers

Il existe deux types de dataTransformers : les modelTransformers et les viewTransformers.

Les premiers permettent de transformer la donnée modèle en donnée normalisée alors utilisable par la vue, et les seconds permettent de transformer la donnée normalisée en donnée vue.

Les données modèle sont celles que l’on récupère par la méthode getData, ou qui sont settées par la méthode setData (méthodes du formulaire)

Les données normalisées sont généralement identiques aux données modèles.

Les données vues sont utilisées pour remplir les champs. La méthode submit($data) du formulaire utilise le format de la vue lors de la soumission des formulaires.

Tout dépend de ce que nous voulons faire. S’il s’agit uniquement de modifier l’information soumise (enregistrer un champ en lettres capitales, par exemple, ou changer le format d’une date), alors ce sont les viewTransformers qu’il faut utiliser.

S’il faut convertir une donnée soumise par le formulaire en un objet, ou en un autre type, alors les modelTransformers sont plus appropriés.

Cas pratique : un champ texte avec un datepicker que nous voulons convertir en date pour la base de données.

class Article
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="publication_date", type="date", length=255, nullable = true)
     */
    private $publicationDate;

...
}

 

Dans un premier temps, il faut déclarer le dataTransformer dans le formType :

->add(
    $builder->create(
        'publicationDate', 'text',
        array(
        'attr' => array('class' => 'datepicker')
        )
    )
    ->addModelTransformer(new TextToDateTimeDataTransformer())
)

 

datepicker

Le dataTransformer étant associé à notre champs dans le formulaire, il faut à présent coder ses deux fonctions : transform et reverseTransform.

1) Le dataTransformer va récupérer le champs texte lors de la soumission du formulaire et le convertir en objet dateTime par la fonction reverseTransform
2) Le dataTransformer, lors de l’édition d’un objet (url du type {objectID}/edit), va récupérer l’information en base de donnée (donc le datetime) et le convertir en champ texte par la fonction transform.

class TextToDateTimeDataTransformer implements DataTransformerInterface
{
    // quand info revient de l'extérieur (base de données, url, fichier texte, etc.), à l'affichage du formulaire lors d'un edit
    public function transform($datetime)
    {
        if (null === $datetime) {
            return '';
        }

        if (!is_object($datetime)) {
            throw new TransformationFailedException('Expected a datetime.');
        }

        return $datetime->format('d').DIRECTORY_SEPARATOR.$datetime->format('m').DIRECTORY_SEPARATOR.$datetime->format('Y');

    }

    // quand le formulaire est soumis
    public function reverseTransform($stringDate)
    {
        if (null === $stringDate) {
            return NULL;
        }

        if (!is_string($stringDate)) {
            throw new TransformationFailedException('Expected a string.');
        }

        return \DateTime::createFromFormat('d/m/Y', $stringDate);
    }
}

 

D’autres cas intéressants peuvent nécessiter l’utilisation d’un dataTransformer : transformer des données soumises sous forme d’array en une chaine de caractère séparée par des virgules (pour le cas de tags d’un article, par exemple), convertir un numéro d’un champ int en un objet enregistré en base, etc.

Imaginons que nous avons un champ texte en base de données, mais que nous avons un champ de type checkbox:

class Article
{
    const FILES_LIMIT = 3;

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="tags", type="string", length=255)
     */
    private $tags;
...
}

tags

Dans un premier temps, il faut déclarer le dataTransformer dans le formType :

->add(
    $builder->create(
        'tags', 'choice',
        array(
        'choices' => array(
            'tag1' => 'tag 1',
            'tag2' => 'tag 2',
            'tag3' => 'tag 3',
            'tag4' => 'tag 4'
        ),
        'required' => false,
        'expanded' => true,
        'multiple' => true,
        'empty_value' => false
        )
    )
    ->addModelTransformer(new ArrayToDelimitedStringDataTransformer())
)

 

Puis nous codons le dataTransformer:

class ArrayToDelimitedStringDataTransformer implements DataTransformerInterface
{
    // quand info revient de l'extérieur (base de données, url, fichier texte, etc.), à l'affichage du formulaire lors d'un edit
    public function transform($string)
    {
        if (null !== $string && !is_string($string)) {
            throw new TransformationFailedException('Expected a string.');
        }
        $string = trim($string);

        if (empty($string)) {
            return array();
        }

        $values = explode(',', $string);
        if (0 === count($values)) {
            return array();
        }

        foreach ($values as &$value) {
            $value = trim($value);
        }

        return $values;
    }

    // quand le formulaire est soumis
    public function reverseTransform($array)
    {
        if (null === $array) {
            return '';
        }

        if (!is_array($array)) {
            throw new TransformationFailedException('Expected an array.');
        }

        $string = trim(implode(',', $array));

        return $string;
    }
}

 

Symfony2 comporte un certain nombre de dataTransformers natifs :

dataTransformers

Le code est disponible sur github: https://github.com/jpsymfony/forms-symfony2

 

Rédigé par

5 comments

  1. Bonjour,

    Je suis un débutant et je voudrais avoir votre avis sur un problème que j’ai sur un projet et sur lequel on m’a conseillé d’utiliser les Data Transfomer.
    En fait je fais une Api ou je gère des commandes de restaurant, mon problème vient du fait que je reçois un objet Commande avec une objet lié composition_commande et un objet user.
    J’utilisais les formulaires jusque la mais me retrouve souvent ralentit par la rigidité du formulaire, quand je reçois mon objet Commande par exemple je dois vérifier mon objet User afin de savoir si il existe déja en BDD ou si je doit le créer.
    Ce qui m’amene souvent à mettre trop de logique dans le controller et a avoir un code peu permissif.
    Du coup, si j’ai bien compris les DataTransformers me permettraient de récuperer l’objet, de pouvoir séparer les infos afin de traiter la partie Commande et User séparément ?
    J’espere m’etre a peu pres bien expliquer.

    Cordialement,

    1. Bonjour,

      Es-tu en API webservices uniquement ou reçois-tu juste un json en post que tu dois traiter? Juste pour savoir si, pour créer la commande, les lignes de commandes et l’utilisateur, il faut appeler plusieurs api ou si l’application est plus classique mais que le point d’entrée est un post par api?

      Il y a plusieurs approches possibles:
      – le composant workflow pour une asynchonicité (mais le but étant que ça se passe vite, je doute que ça soit une bonne idée. On l’emploierait davantage dans un process où il n’est pas grave de relancer une étape, comme la création de la commande ou la création des lignes de commandes)
      – les events (idem, si un des trois events se plante, on a une incohérence)
      – un formulaire de type commande avec un user imbriqué et des produits par quantité imbriqués (donc de type collection), ce qui n’est pas mal mais va effectivement t’obliger à utiliser des dataTransformer en cascade (un pour récupérer les infos de l’utilisateur et en retourner un objet par le reverseTransform par exemple), mais très sincèrement, je ne suis pas sûr que ce soit une bonne idée
      – des services => c’est davantage vers cela que je m’orienterais (avis bien évidemment personnel)

      Personnellement, je recevrai donc ce gros json en post dans mon contrôleur. Je créerais un Handler qui va récupérer la request et appeler un User Manager. Celui-là aurait une méthode getUser à qui on lui passerait les infos de la request et il s’occuperait de retourner un user (nouveau ou de la base).

      Ensuite, toujours dans ce même commande handler, j’appelerais un commandeManager et je lui passerais la request et le user. Ce manager se chargerait de faire appel à deux repositories (un pour les lignes de commande, un pour la commande en elle-même), créerais la commande, les lignes de commande, les rattacherais à la commande en elle-même, et associerais le user à la commande.

      Et je retournerais un objet commande créé.

      Après, on peut encore aller plus loin, mais je trouve que c’est sortir le bazooka: chaine de responsabilité. On fait passer la request d’un service à un autre, et chacun en retire ce qu’il doit retirer: le premier maillon créé le user, le second la commande, le troisième les lignes de commande et celui-ci. Le premier maillon serait top et léger, le second devrait cependant aller récupérer le user pour l’y associer (mais on sait qu’il serait en base), et le troisième devrait trouver la commande par son id (il faudrait donc forcément que l’on te donne l’id de commande dans le json).

      Mais, instinctivement, j’utiliserais plutôt les services car c’est la solution la plus rapide et qui reste tout de même souple pour l’avenir. Et surtout, tu vas pouvoir utiliser les transactions doctrine (je suppose que tu es en doctrine) pour que tout passe ou rien du tout (ce qui évite de se retrouver avec des orphelins en base de données)

  2. Bonjour,

    Tout d’abord merci du temps pris afin de me répondre et pour ces vrais conseils.

    « Es-tu en API webservices uniquement », la je n’ai pas vraiment compris ce que entendez par la!
    Mais effectivement je reçois un Json en Post qui va me permettre de créer directement la commande, sa composition, et lier un utilisateur ( a creer ou a trouver ) donc tout ça sur un seul appel de l’API.

    Je vais plus me pencher sur les services alors, je n’ai encore jamais utilisé les DTO et je n’était déja pas sur que ce soit la meilleure solution a base.
    Concernant les chaines de responsabilités je n’en suis pas encore je pense qu’un service m’ira tres bien!

    En tout cas encore une fois un grand merci pour cette réponse claire et détaillée !

    1. Pour les API webservices uniquement, je voulais dire que tout le site tourne autour d’API (donc des routes accessibles en GET/POST/PUT/PATCH/DELETE) avec envoi de payload (corps de la requête) en json (c’est ce qui est le plus courant) et retour en json/xml. Quand un site est construit ainsi, tout le site « consomme » les API (via des appels ajax avec des frameworks JS comme angular, react ou backbone)

      Donc, par exemple, pour une mise à jour de commande, on ferait un GET sur /user/{id_user} pour récupérer les informations du user de la commande (ou obtenir une 404 s’il n’existe pas), puis on ferait un POST /commande (ou un PUT si mise à jour d’une commande précédente) etc. Quand on a un site plus monolythique, ces appels API sont plutôt des requêtes repository.

  3. Je suis plus sur un site monolythique alors en effet.
    Apres le but est par la suite de passer sur une API web service uniquement pour justement séparer le front et travailler avec angular que j’aimerai decouvrir !

    La ou j’ai du mal avec ce gros Json c’est de separer les divers elements de la requete afin de les faire passer sois au UserManager ou CommandeManager.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.