Exemple d'utilisation du composant serializer de Symfony2: création d'un sitemap

Le but n'est pas de présenter le composant serializer de Symfony2 , la documentation officielle est là pour ça. Il existe egalement un présentation assez complète faite par Hugo Hamon lors d'un Symfony Live.

L'idée est de montrer un exemple d'utilisation, et par celui-ci, montrer quelques astuces pour modifier le résultat. J'ai du farfouiller le code du composant pour trouver ces infos alors je me dis que cela pourra peut être interesser quelqu'un d'autre.

Nous allons donc créer un sitemap dynamique pour un siteweb. Les morceaux de code proviennent d'un site en Symfony2 mais cela pourrait fonctionner de la même manière avec uniquement le composant.

Données de depart

Nous avons une route /sitemap qui match une action dans le controller sitemapAction.
Nous avons une class Url qui contient les données pour le sitemap:

class Url
{
    /**
     * Url complète de la page
     * 
     * @var string
     */
    protected $loc;
 
    /**
     * Date de dernière modification de la page (format Y-m-d)
     * 
     * @var string
     */
    protected $lastmod;
 
    /**
     * Priorité de la page
     * 
     * @var float
     */
    protected $priority;
 
    //GETTERS et SETTERS
}
 
 

Initialisation de l'action

Je ne détaille pas la récupération des objets Url car cela n'a aucun intérêt pour l'exemple.

    public function sitemapAction()
    {
        //initialisation du serializer
        $encoders = array(new XmlEncoder(), new JsonEncoder());
        $normalizers = array(new GetSetMethodNormalizer());
        $serializer = new Serializer($normalizers, $encoders);
 
        //recuperation des objets Url dans la variable $urls
       // $urls -> tableau indexé d'objets de type Url
 
        $response = new Response();
        $response->setContent($serializer->serialize($urls, 'xml'));
        $response->headers->set('Content-Type', 'application/xml');
 
        return $response;
    }

On obtient le résultat suivant:

<response>
    <item key="0">
        <loc>http://url1</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </item>
    <item key="1">
        <loc>http://url2</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </item>
    <item key="2">
        <loc>http://url3</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.5</priority>
    </item>
</response>

Il y a deux problèmes pour respecter la normalisation des fichiers sitemap:
- la balise principale est par défaut "response" alors qu'on veut "urlset"
- les différentes urls ne sont pas dans une balise "url" mais "item" avec un attribut "key" qui nous interesse pas.

Surcharger la balise principale

Première fouille dans le code, et plus précisement dans l'encoder XML: Symfony\Component\Serializer\Encoder\XmlEncoder:

class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface
{
    private $dom;
    private $format;
    private $rootNodeName = 'response';
 
    /**
     * Construct new XmlEncoder and allow to change the root node element name.
     *
     * @param string $rootNodeName
     */
    public function __construct($rootNodeName = 'response')
    {
        $this->rootNodeName = $rootNodeName;
    }

Facile à trouver, il suffit de passer un paramètre dans le constructeur:

//extrait de la méthode sitemapAction du controller
 
$encoders = array(new XmlEncoder('urlset'), new JsonEncoder());

Notre xml devient donc:

<urlset>
    <item key="0">
        <loc>http://url1</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </item>
    <item key="1">
        <loc>http://url2</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </item>
    <item key="2">
        <loc>http://url3</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.5</priority>
    </item>
</urlset>

Avoir plusieurs balises avec le même nom

Toujours dans la même classe, en regardant la construction du xml de plus près, voici ce que l'on trouve:

    /**
     * Parse the data and convert it to DOMElements
     *
     * @param DOMNode      $parentNode
     * @param array|object $data       data
     * @param string       $xmlRootNodeName
     *
     * @return Boolean
     *
     * @throws UnexpectedValueException
     */
    private function buildXml($parentNode, $data, $xmlRootNodeName = null)
    {
        $append = true;
 
        if (is_array($data) || $data instanceof \Traversable) {
            foreach ($data as $key => $data) {
                //Ah this is the magic @ attribute types.
                if (0 === strpos($key, "@") && is_scalar($data) && $this->isElementNameValid($attributeName = substr($key, 1))) {
                    $parentNode->setAttribute($attributeName, $data);
                } elseif ($key === '#') {
                    $append = $this->selectNodeType($parentNode, $data);
                } elseif (is_array($data) && false === is_numeric($key)) {
                    /**
                     * Is this array fully numeric keys?
                     */
                    if (ctype_digit(implode('', array_keys($data)))) {
                        /**
                         * Create nodes to append to $parentNode based on the $key of this array
                         * Produces <xml><item>0</item><item>1</item></xml>
                         * From array("item" => array(0,1));
                         */
                        foreach ($data as $subData) {
                            $append = $this->appendNode($parentNode, $subData, $key);
                        }
                    } else {
                        $append = $this->appendNode($parentNode, $data, $key);
                    }
                } elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
                    $append = $this->appendNode($parentNode, $data, "item", $key);
                } else {
                    $append = $this->appendNode($parentNode, $data, $key);
                }
            }
 
            return $append;
        }

Au milieu de ce code par forcement très lisible, un commentaire :

/**
 * Create nodes to append to $parentNode based on the $key of this array
 * Produces <xml><item>0</item><item>1</item></xml>
 * From array("item" => array(0,1));
 */
 

Donc, tout ce qu'il nous faut c'est mettre notre tableau $urls dans un autre tableau avec comme clé 'url'.
Notre action devient donc:

    public function sitemapAction()
    {
        //initialisation du serializer
        $encoders = array(new XmlEncoder('urlset'), new JsonEncoder());
        $normalizers = array(new GetSetMethodNormalizer());
        $serializer = new Serializer($normalizers, $encoders);
 
        //recuperation des objets Url dans la variable $urls
        // $urls -> tableau indexé d'objets de type Url
        $map['url'] = $urls;
 
        $response = new Response();
        $response->setContent($serializer->serialize($map, 'xml'));
        $response->headers->set('Content-Type', 'application/xml');
 
        return $response;
    }

Et notre sitemap est créé:

<urlset>
    <url>
        <loc>http://url1</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>http://url2</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>http://url3</loc>
        <lastmod>2013-03-10</lastmod>
        <priority>0.5</priority>
    </url>
</urlset>

Conclusion

Ceci est un exemple assez simple au final.
Mais l'idée était de montrer qu'on peut faire des petits trucs sympas assez facilement avec le composant Serializer de Symfony2 et que, très souvent, il suffit de fouiller et de lire un peu le code source pour résoudre la plupart des problèmes.

Il y a 4 commentaires.

Ecrit par Mikl le 14 juin 2013

pour optimiser un peu ton code il faudrait rajouter dans le routage la propriété: _format: xml ( pour l'exemple ) ca évite de devoir spécifier l'entete content-type xml. Ce qui donne au final : return new Response($serializer->serialize($map, 'xml')); au lieu de : $response = new Response(); $response->setContent($serializer->serialize($map, 'xml')); $response->headers->set('Content-Type', 'application/xml'); return $response;

Ecrit par Kevin le 11 avr. 2013

Merci, j'ai trouvé ça tellement sympa que j'en ai fait un petit repo... Regarde la "todo list" en bas de README.md; une idée pour dynamiquement ignorer les éléments vides? Ce serait peut-être un ajout à faire au composant Serializer.
https://github.com/kbsali/sitemap-serializer

Ecrit par Pascal le 9 avr. 2013

sympa l'article, une petite typo : piority -> priority

Réponse de Muriel le 10 avr. 2013

Oups!! Merci

Ajouter un commentaire