Serializer le barbare
Dernière je me suis retrouvé confronté à l’un des pires problèmes de ma vie de dev : la désérialisation. Derrière ce mot barbare, que le dictionnaire de Firefox ne semble pas reconnaître (d’ailleurs il n’apprécie pas plus son équivalent anglais « deserialization ») se cache un processus pourtant fort utile : la transformation d’un fichier contenant des données en entités objets.
En réalité la désérialisation va de paire avec la sérialisation, l’opération inverse (transformation d’entités en un fichier de données). On peut également passer par une étape intermédiaire, celle d’un tableau de données, ce qui ajoute alors les opérations d’encodage, de décodage, de normalisation et de dénormalisation.
Ceux qui ont déjà eu l’occasion de se frotter au composant « Serializer » de Symfony2 ont certainement eu l’occasion de lire ce superbe schéma récapitulatif du workflow que je viens d’évoquer :
Du XML trop complexe pour Symfony et JMS
Ce que je voulais faire était simple : un de nos logiciels métiers me fournit un fichier unique XML contenant plusieurs entités (jusqu’à plusieurs milliers, en fait) nommées « insertions ». Chaque insertion possède plusieurs relation ManyToOne comme, par exemple, des factures, des règlements, etc… Au final c’est un peu ce genre de schéma que je recevais :
Ce XML au format relativement simple pose pourtant de sérieux problèmes au serializer de Sf2. Notamment car :
- Certaines données ne sont pas directement dans les nœuds, mais dans les attributs,
- Il y a plusieurs « insertions » dans un seul fichier XML.
Or le serializer est intransigeant : il attend des nœuds simples, et une seul entités par fichier. Pratique ! J’ai donc tenté de me tourner vers le fameux JMSSerializerBundle, sans succès. Celui-ci me promettait pourtant le paradis : gestion des données en attributs et possibilité d’avoir une liste d’entités. Mais j’ai vite déchanté : mon cas était encore trop complexe.
En effet, nativement le JMS permet de gérer les données en attribut direct d’une entité, mais pas dans les attributs d’un composant d’une entité… Si je vous ai perdu, voici un petit exemple pour vous faire comprendre la lacune. Imaginons un codeur qui porte un pantalon dont on veut stocker la couleur. JMS va gérer le cas où dans le XML l’attribut « couleurPantalon » est directement au sein du nœud « codeur » :
Mais pas le cas où on a un nœud « pantalon » avec l’attribut « couleur » :
La solution résiderait alors dans la création d’une entité « pantalon » lié par un OneToOne à mon entité « codeur », ce qui ne nous arrange absolument pas avouez-le. Si l’utilisation d’un unique attribut dans l’entité « codeur » est justifiée, la construction du XML avec un nœud dédié « pantalon » l’est aussi. Alors que faire ?
Le denormalizer maison
Template du fichier XML
Je me suis finalement tourné vers une solution proposée sur ma question StackOverflow, à savoir créer mon propre « dénormalisateur ». Mais bien plus enrichi que ce qui est proposé en solution sur le site de SO. L’idée est simple : laisser le sérializer gérer le décodage en un tableau, et ensuite parcourir moi-même ce tableau afin de générer mes entités. Ça peut sembler une vraie usine à gaz, et c’est normal car ç’en est une, mais aux grands maux les grands remèdes !
Voilà mon idée : je finis un modèle de mon fichier XML dans le format que je veux. Personnellement j’ai choisis le YAML que j’affectionne pour sa simplicité de lecture comme d’écriture. Dans le cas de notre insertion, il ressemblerait à ceci :
Vous avez compris le principe : je définis l’arborescence du XML et, dès que j’arrive à un noeud contenant des données, je lui attribue deux paramètres :
- &type : le format des données (string, datetime, etc…) qui me servira plus tard à les traiter,
- &setter : le nom de la methode setter de l’entité qui servira à insérer la donnée dans celle-ci.
Vous remarquez également le @couleur du noeud « pantalon » : cela signifie que la donnée se trouve dans l’attribut « couleur » de son nœud parent.
Arobase comme dans @ttribut
Maintenant que le template est prêt, voyons le reste. On crée notre propre dénormalizer en lui passant en paramètre ce qu’on souhaite, mais au moins une instance de Doctrine afin qu’il puisse faire ses propres insertions en base.
À partir de là, l’idée est la suivante : on réécrit la méthode denormalize() (à laquelle on injecte en paramètre le YAML schéma) au sein de laquelle on parcourt le tableau issu du XML décodé :
Comme vous le voyez, cette méthode appelle en fait une méthode « treatEntity() » donc le but va être de traiter chaque noeud représentant une de nos entités en base (ici un codeur). Cette méthode va alors faire deux choses : elle va commencer par traiter les attributs directs de l’entité si elle en a, puis elle appellera une méthode itérative qui se chargera de traiter le reste de l’arborescence.
Vous avez immédiatement noté l’appel à la méthode itérative treatNode() qui va ensuite traiter le tableau étage après étage. Par contre, deux choses ont du vous interpeler : tout d’abord, on cherche le caractère ‘@’ dans le nom des noeuds. En effet, c’est la subtilité du serializer : les noms des attributs sont précédés d’un arobase afin de les reconnaître. Le second point, c’est l’utilisation d’une nouvelle variable $map ; en fait, l’efficacité du processus est basée sur le fait qu’on avance en parallèle dans le tableau de données et dans le schéma YAML. Cela nous permet de diminuer, à chaque étape du tableau, la quantité de données à évaluer via des boucles.
Dès lors, le chemin de la méthode treatNode() est tout tracé car c’est quasiment la même que la précédente :
- soit le nœud courant est sans enfants, commence par un @ (attribut) ou par un # (donnée du nœud ) auquel cas on envoie le nœud à une méthode chargée de l’évaluer et de le transformer en donnée de l’entité
- soit le nœud contient des enfants, auquel cas la méthode se relance elle-même en passant au niveau enfant du nœud de base.
Nous touchons au but avec un dernier appel à la méthode « updateCodeur() » qui va -enfin- analyser les données du nœud et les insérer dans l’entité Codeur. Les points clés de cette méthode sont, tout d’abord, la gestion de différents types de données et leur transformation (notamment Datetime). Ensuite, il y a la gestion des données de type « entity » qui sont, finalement, un appelle à ce même denormalizer. Enfin, une fois la valeur du nœud traitée, elle appelle $codeur->$setter où $setter est le nom du setter trouvé dans le YAML schéma.
Et voilà ! De manière itérative, chaque fois qu’on arrive à une fin de nœud, on insère la valeur dans l’entité. Celle-ci remonte alors finalement jusqu’à la méthode denormalize() du début et est alors insérée en base.
Dernière précision : si vous avez tout lu vous avez noté l’appel à « treatBasicEntity()« . En réalité, je n’ai pas géré le cas où les entités liées par relations à « Codeur » possèderaient également un XML spécial. Je suis parti du principe qu’elles ne contenaient que des données simples ou des attributs. Son unique boulot est donc de transformer les attributs en noeuds standards de manière à ce que la méthode denormalize() de base du serializer soit capable de l’interpréter.
Je conçois que ce post était long et, certainement, imbuvable. Néanmoins si quelqu’un y trouve un intérêt, qu’il me le fasse savoir, ça fait toujours plaisir !