PrestaConcept
Nos réalisations
Nos métiers
  • Découvrez nos métiers
  • Développement web sur mesure

    Nous développons en méthode agile des back sous le framework PHP Symfony, des front en Angular.

  • Maintenance d'applications

    Maintien en condition opérationnelle de votre plateforme Symfony.

  • Expertise Symfony

    Coaching, formation, audit et conseil.

  • Hébergement et Infogérance

    Une expertise de l'hébergement depuis plus de 15 ans et l’infogérance de centaines de machines en production actuellement.

  • Qui sommes nous
  • Découvrez Prestaconcept
  • PrestaConcept

    Notre histoire, nos convictions, notre vision... Découvrez ce qui nous anime !

  • L'équipe

    Plus de 15 ans minimum d'expérience sur Symfony.

  • Nos engagements RSE

    Une société engagée pour un numérique responsable.

  • Nos convictions

    Co-construction, transparence.. Les principes qui guident nos collaborations !

  • Nous rejoindre

    Envie de nous rejoindre ? Consultez nos offres !

  • Blog
    J'ai un projet Nous contacter
    J'ai un projet Nous contacter Menu
    • Accueil
    • Blog
    • Tech
    • Librarie Open Source : PrestaSitemapBundle

    Blog

    Librarie Open Source : PrestaSitemapBundle

    bundle open source php sitemap symfony
    Yann Eugoné
    Yann Eugoné CTO
    Publié le vendredi 23 octobre 2020

    Le bundle OpenSource made by Prestaconcept qui génère le sitemap de votre projet Symfony

    Besoin

    Avoir un sitemap est quelque chose de commun, il y a quelques années, c'était même un pré-requis pour accéder à un bon niveau de référencement (aujourd'hui c'est facultatif, puisque les robots d'indexation peuvent construire le plan en naviguant sur votre site).

    Ainsi, dès lors que l'on construit une application avec une partie publique, faire un plan est toujours une bonne idée.

    Problématique

    Un sitemap n'est rien d'autre qu'un XML censé représenter au mieux la structure de votre site. C'est donc un fichier structuré, validé.

    On peut l'écrire à la main. A l'initialisation, ça ne prendra pas beaucoup de temps (si l'application n'est pas trop grosse), mais il faudra le maintenir par la suite. Et ça, en revanche, ça sera chronophage et source d'erreurs.

    Solution

    Puisque ce XML obéit à des règles et qu'elles sont communes à tous les projets, nous devrions être en mesure de proposer une méthode pour générer ce fichier.

    C'est là qu'est né ce bundle (même si à l'origine, c'était un plugin Symfony 1.x...).

    La promesse est simple : intégrer à votre application Symfony une façon simple et efficace de générer votre plan de site, avec un effort moindre.

    Vous pouvez sans attendre le retrouver sur GitHub ou Packagist .

    note : Vous pouvez également utiliser le projet de démo pour tester le bundle : GitHub

    Installation

    Ajoutez le bundle en dépendance de votre projet.

    composer require presta/sitemap-bundle
    

    Si vous n'êtes pas encore passés sous symfony/flex (vous devriez y songer), ajoutez le bundle à votre kernel.

    new Presta\SitemapBundle\PrestaSitemapBundle(),
    

    Importez les routes du bundle.

    PrestaSitemapBundle:
        resource: "@PrestaSitemapBundle/Resources/config/routing.yml"
    

    Voilà, c'est installé, reste à configurer les routes que vous souhaitez exposer.

    Cas d'usage

    Imaginons que votre application ait quelques routes publiques :

    • / : la page d'accueil (merci capitaine)
    • /faq : une foire aux questions
    • /contact : un formulaire de contact
    • /offre/{offerSlug} : une page d'offre
    • /blog/{categorySlug} : une page de liste de billets de blog pour une catégorie donnée
    • /blog/{categorySlug}/{postDate}/{postSlug} : une page de billet de blog

    Je pars du principe que votre application existe déjà, que vos contrôleurs et vos routes sont configurés, etc... Dans cet article nous n'allons détailler que les spécificités du bundle.

    Routes statiques

    Nous appelons "route statique" une route n'ayant aucun paramètre requis. Pour la construire, le nom de la route suffit.

    class StaticController
    {
        /**
         * @Route("/", name="homepage", options={"sitemap" = true})
         */
        public function homepageAction() { /* ... */ }
    
        /**
         * @Route("/faq", name="faq", options={"sitemap" = true})
         */
        public function faqAction() { /* ... */ }
    
        /**
         * @Route("/contact", name="contact", options={"sitemap" = true})
         */
        public function contactAction() { /* ... */ }
    }
    

    note : si vous n'aimez pas les annotations, sachez que vous pouvez configurer ces options en YAML ou en XML

    En quelques caractères, on a déjà couvert les 3 premières routes de notre liste. Pour les routes statiques, il suffit de configurer l'option "sitemap" = true, et le tour est joué.

    note : "sitemap" = true est un raccourci qui enregistre les options par défaut pour la route. Vous pouvez détailler toutes ces options si vous le souhaitez. Voir la documentation.

    Vous pouvez déjà vous rendre sur /sitemap.default.xml et constater que vos pages sont présentes.

    Routes dynamiques

    Nous appelons "route dynamique" une route ayant au moins un paramètre requis. Pour la construire, il faut donner une valeur à chacun de ces paramètres, et vous seuls pouvez donner ces valeurs.

    Comme souvent, vous allez vouloir stocker tout ça dans une base de données, probablement SQL et que vous allez certainement utiliser l'ORM Doctrine comme intermédiaire (je sais, j'ai un don).

    class BlogController
    {
        /**
         * @Route("/blog/{categorySlug}", name="blog-post-list-by-category")
         */
        public function listByCategoryAction(string $categorySlug) { /* ... */ }
    
        /**
         * @Route("/blog/{categorySlug}/{postDate}/{postSlug}", name="blog-post-detail")
         */
        public function detailAction(string $categorySlug, DateTimeInterface $categorySlug, string $postSlug) { /* ... */ }
    }
    
    class CommerceController
    {
        /**
         * @Route("/offre/{offerSlug}", name="offer-detail")
         */
        public function offerAction(string $offerSlug) { /* ... */ }
    }
    

    note : ce bundle n'a aucun lien avec Doctrine ORM, ni avec les bases de données en général. Les paramètres des routes peuvent provenir de sources diverses et variées, mais le problème reste entier.

    Pour alimenter notre sitemap avec nos routes dynamiques, nous allons utiliser un event subscriber.

    use Doctrine\ORM\EntityRepository;
    use Presta\SitemapBundle\Event\SitemapPopulateEvent;
    use Presta\SitemapBundle\Service\UrlContainerInterface;
    use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
    use Symfony\Bridge\Doctrine\RegistryInterface;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\Routing\RouterInterface;
    
    class SitemapListener implements EventSubscriberInterface
    {
        private $doctrine;
        private $router;
    
        public function __construct(RegistryInterface $doctrine, RouterInterface $router)
        {
            $this->doctrine = $doctrine;
            $this->router = $router;
        }
    
        public static function getSubscribedEvents(): array
        {
            return [
                SitemapPopulateEvent::ON_SITEMAP_POPULATE => 'buildSitemap',
            ];
        }
    
        public function buildSitemap(SitemapPopulateEvent $event): void
        {
            if (in_array($event->getSection(), ['default', null], true)) {
                $this->registerOffers($event->getUrlContainer());
            }
            if (in_array($event->getSection(), ['blog', null], true)) {
                $this->registerBlog($event->getUrlContainer());
            }
        }
    
        private function registerOffers(UrlContainerInterface $sitemap): void
        {
            foreach ($this->iterate(Offer::class) as $offer) {
                $sitemap->addUrl(
                    $this->url(
                        'offer-detail',
                        ['offerSlug' => $offer->getSlug()],
                    ),
                    'default'
                );
            }
        }
    
        private function registerBlog(UrlContainerInterface $sitemap): void
        {
            foreach ($this->iterate(BlogPostCategory::class) as $category) {
                $sitemap->addUrl(
                    $this->url(
                        'blog-post-list-by-category',
                        ['categorySlug' => $category->getSlug()]
                    ),
                    'blog'
                );
            }
    
            foreach ($this->iterate(BlogPost::class) as $post) {
                foreach ($post->getCategories() as $category) {
                    $sitemap->addUrl(
                        $this->url(
                            'blog-post-detail',
                            [
                                'categorySlug' => $category->getSlug(),
                                'postDate' => $post->getDate()->format('Y-m-d'),
                                'postSlug' => $post->getSlug(),
                            ]
                        ),
                        'blog'
                    );
                }
            }
        }
    
        private function url(string $route, array $parameters = []): UrlConcrete
        {
            return new UrlConcrete(
                $this->router->generate($route, $parameters, RouterInterface::ABSOLUTE_URL)
            );
        }
    
        private function iterate(string $class): \Generator
        {
            /** @var EntityRepository $repository */
            $repository = $this->doctrine->getManager()->getRepository($class);
            foreach ($repository->createQueryBuilder('o')->getQuery()->iterate() as $result) {
                yield $result[0];
            }
        }
    }
    

    Détaillons un peu tout ce code.

    • getSubscribedEvents nous vient de EventSubscriberInterface elle sert juste à "binder" la méthode buildSitemap sur l'événement que le bundle va déclencher
    • buildSitemap est appelée lorsque le bundle génère le sitemap. A ce moment on pourra y ajouter des éléments. On a découpé notre méthode en 2, histoire de garder le code un peu clair (ce listener peut devenir bien costaud si votre application grossit, en faire plusieurs peut aussi être une idée). Vous aurez remarqué que l'appel à ces méthodes est conditionné, c'est une affaire d'optimisation, afin d'éviter de charger des données inutiles pour des routes non demandées. La "section" vaudra null lorsqu'il sera question de dumper tout le sitemap, et le nom de la section lors du dump d'une section en particulier.
    • registerOffers, registerBlog sont nos méthodes principales, elles itèrent sur les entités enregistrées, et ajoutent des URLs au sitemap
    • url est une méthode factorisée. Elle permet de fabriquer les objets UrlConcrete, en utilisant le router pour remplir notre sitemap
    • iterate est une méthode factorisée. Elle met à disposition de son appelant un itérateur sur une entité passée en paramètre

    note : j'ai fait le choix d'un event subscriber, une histoire de préférences, libre à vous d'écrire un listener à la place (ça ne change pas grand-chose de toute façon).

    Si vous êtes sur un Symfony < 3.3 (ou si vous avez choisi de ne pas utiliser le PSR4 service discovery), vous devrez créer un service pour votre listener.

    services:
        sitemap.listener:
            class: SitemapListener
            tags:
                - { name: kernel.event_subscriber }
    

    A ce moment-là, si vous demandez l'URL /sitemap.xml vous verrez qu'il y a 2 sections dans votre application /sitemap.default.xml & /sitemap.blog.xml. Si vous avez l’œil, vous aurez remarqué cela vient du fait qu'on ait ajouté les URLs à la section "blog" dans notre listener.

    Ajouter plus que des URLs

    Un sitemap peut contenir bien plus que les URLs des pages de votre application. Vous pouvez y intégrer d'autres ressources, comme les images ou les vidéos présentes dans les pages.

    Imaginez par exemple que vos billets de blogs aient des images, si vous souhaitiez les intégrer à votre plan, vous pourriez faire comme ça :

    /* ... */
    use Presta\SitemapBundle\Sitemap\Url\GoogleImage;
    use Presta\SitemapBundle\Sitemap\Url\GoogleImageUrlDecorator;
    
    class SitemapListener /* ... */
    {
        /* ... */
        private function registerBlog(UrlContainerInterface $urlContainer): void
        {
            /* ... */
            foreach ($this->iterate(BlogPost::class) as $post) {
                foreach ($post->getCategories() as $category) {
                    $url = $this->url(
                        'blog-post-detail',
                        [
                            'categorySlug' => $category->getSlug(),
                            'postDate' => $post->getDate()->format('Y-m-d'),
                            'postSlug' => $post->getSlug(),
                        ],
                    );
    
                    $images = $post->getImages();
                    if (count($images) > 0) {
                        $url = new GoogleImageUrlDecorator($url);
                        foreach ($images as $idx => $image) {
                            $url->addImage(
                                new GoogleImage($image, sprintf('%s - %d', $post->getTitle(), $idx + 1))
                            );
                        }
                    }
    
                    $urlContainer->addUrl($url, 'blog');
                }
            }
        }
        /* ... */
    }
    

    note : il existe plusieurs décorateurs pour ajouter des informations à vos routes. Vous les retrouverez tous dans la documentation

    Dumper le sitemap

    Nous y voilà, vous avez fini d'implémenter toutes les règles qui permettent à votre sitemap de se générer. Et lorsque vous appelez l'URL en question, celui-ci se rend sans difficulté.

    Reste une dernière petite chose que vous pouvez faire. En effet, pour le moment, votre sitemap est construit à la demande. Ce qui signifie que chaque fois que vous demandez quelque chose comme /sitemap.xml ou /sitemap.{section}.xml, vous atterrissez sur un contrôleur Symfony appartenant à notre bundle : (contrôleur & routing).

    À ce moment-là, tout se met en place :

    • les routes statiques sont collectées
    • les listeners sont appelés

    À chaque fois !

    Pour une petite application, c'est acceptable. Mais pour un site contenant des milliers de pages, pour la plupart dynamiques, ça veut dire beaucoup de temps perdu à faire les mêmes opérations en boucle...

    Pour pallier ce problème de performance, le bundle propose une commande permettant de "dumper" les éléments de votre sitemap. Comprenez que chaque élément (index & sections) va être enregistré sous forme de fichier et déposé dans la partie publique de votre application (web/ ou public/). Ainsi, si le fichier est présent, il sera rendu directement à l'appelant, sans avoir à le recalculer et donc sans avoir à interroger Symfony.

    php bin/console presta:sitemaps:dump
    

    C'est typiquement le genre de commande que vous allez vouloir appeler de manière régulière (via un CRON par exemple) pour reconstruire le plan de votre application.

    Note : Les URLs ajoutées au sitemap doivent être absolues. Pour que cela puisse se faire depuis une commande, vous devrez indiquer au framework votre contexte

    Conclusion

    Ce qu'il faut retenir :

    • Une option "sitemap" à ajouter sur les routes statiques
    • Un event listener classique pour les routes dynamiques
    • Une commande pour persister les éléments du plan et gagner en temps d'exécution

    J'espère que la promesse est tenue !

    note : le site que vous êtes entrain de parcourir dispose d'un sitemap, et il est généré par ce bundle.

    Blog

    Pour continuer votre lecture ...

    Tech

    Librarie Open Source : YokaiEnumBundle

    Par Yann Eugoné 04/06/2021

    Un bundle OpenSource qui apporte un système d'énumération totalement intégré à votre projet Symfony.

    Lire la suite
    Tech

    Des filtres enregistrés dans vos admins Sonata

    Par Benoit Jouhaud 07/12/2023

    Le PrestaSonataSavedFiltersBundle est un bundle Symfony qui permet de gérer des filtres enregistrés pouvant être appliqués aux listes des admins Sonata afin de les restaurer dans un état précis.
    Il s'intègre aux écrans de liste via un menu situé en haut à droite de l'écran, et dispose d'une admin p...

    Lire la suite
    Tech

    Librarie Open Source : PrestaImageBundle

    Par Benoit Jouhaud 03/03/2021

    Le bundle open source made by Prestaconcept qui permet à vos utilisateurs de redimensionner une image à l'upload.

    Lire la suite

    Vous avez un projet Laravel ?

    Nous sommes spécialisés en Symfony, et grâce à Web^ID, l’agence sœur du groupe Agile Invest, nous couvrons aussi toute l’expertise Laravel.

    Découvrir Web^ID

    Une question, un projet ?
    Planifiez un échange avec nous !

    Choisissez votre date
    PrestaConcept - Groupe Agile Invest
    5, imp. Morel, 69003 Lyon +33 (0)4 78 54 45 45
    Suivez-nous
    Ecoindex B

    Ce site internet est un site basse consommation. En savoir plus sur l'Ecoindex

    Nos réalisations

  • Logiciel de mise en conformité réglementaire
  • Application de suivi de production des centrales éoliennes
  • Outil d'aide à la décision
  • Portail client
  • Nos métiers

  • Développement sur-mesure
  • Reprise d'application Symfony
  • Expertise Symfony
  • Hébergement & Infogérance
  • Qui sommes-nous

  • PrestaConcept
  • Groupe Agile Invest
  • L'équipe
  • Engagement RSE
  • Blog

  • Tech
  • Méthodologie
  • PrestaConcept
  • RSE
  • © 2025 PrestaConcept
    Mentions légales Politique de confidentialité 🍪
    Retour en haut de page