Contactez-nous

Kitpages
17 rue de la Frise
38000 Grenoble
tel : 04 58 00 33 81

Par Philippe Le Van (twitter accountplv) Dernière mise à jour : 18 janvier 2012

Extensibilité d'un bundle symfony2

Introduction

Quand on fait un bundle réutilisable pour Symfony2, on doit se demander comment l'utilisateur du bundle pourra modifier le comportement ou l'affichage du bundle de la façon la plus simple possible.

Il y a plusieurs modes d'extensions possible, nous allons les explorer ici.

Cette page propose quelques principes et organisations que nous allons suivre quand c'est possible dans les futurs bundles de Kitpages.

 

Quelques principes de base

Il y a de nombreux points d'extension possibles sur un bundle symfony2. Certains sont plus simples que d'autres. Nous classons ici notre ressenti vis-à-vis de la complexité de mise en oeuvre des méthodes d'extension

  • Le plus simple : les configurations dans config.yml
    • Un paramètre configurable ajouté dans la configuration du DIC n'ajoute aucun fichier et c'est très simple à utiliser
  • Simple : surcharger des templates twig
    • Il suffit de déposer un template twig du même nom dans le répertoire app/Resources/MonBundle/toto/titi.html.twig et l'application prend ce template à la place du template du bundle.
  • Assez simple : écouter des évènements
    • Si le bundle envoie des évènements, ça peut être un moyen simple d'étendre les fonctions du bundle, voire de remplacer les fonctions du bundle si les évènements sont "defaultPreventable"
    • Ca demande de créer des configurations dans le DIC + créer une classe qui écoute
    • Notons que ce type d'extension nécessite 2 choses :
      • Le bundle doit envoyer des évènements (potentiellement "defaultPreventable")
      • Les évènements doivent être documentés
  • Assez complexe : remplacer un service par son propre service
    • C'est un paramètre à changer dans la configuration + un service du bundle à étendre dans son application
    • Ca demande une connaissance interne du fonctionnement du bundle
  • Complexe : surcharger un bundle
    • Ca demande de créer un bundle dans son application qui a pour parent le bundle que l'on souhaite étendre.
    • Ca multiplie les répertoires et les fichiers, c'est un bazouka qu'il ne faut utiliser que quand on n'a pas le choix
    • C'est à réserver comme mode d'extension que pour des éléments qui peuvent être centraux dans l'application (Exemple, le FOSUserBundle...)

Passées les considérations théoriques ci-dessus, voyons comment nous allons organiser notre bundle pour le rendre plus facilement extensible.

Exemples avec le KitpagesShopBundle

Dans les exemples ci-dessous, on utilise le KitpagesShopBundle comme nom de bundle.

Les configurations dans config.yml

En gros il faut regarder dans des bundles existants les 2 fichiers suivants :

  • Kitpages/ShopBundle/DependencyInjection/Configuration.php
    • Parse les configurations spéficiques au bundle dans le fichier config.yml
  • Kitpages/ShopBundle/DependencyInjection/KitpagesShopExtension.php
    • Permet de "mapper" certaines configurations avec des paramètres

Je ne rentre pas plus dans le détail, regardez des exemples de bundle existants. Ces configurations feraient l'objet d'un tutoriel complet.

Organisation des templates twig

Nos bundle vont intégrer systématiquement un template Ressources/views/layout.html.twig qui va ressembler à l'exemple suivant :

un layout "KitpagesShopBundle::layout.html.twig"

{% extends "::base.html.twig" %}

{% block body %}
    <!-- inclusion éventuelle des styles spéficiques au bundle -->
    <link rel="stylesheet" type="text/css" href="/bundles/kitpagesshop/css/common.css" />

    <!-- inclusion éventuelle des javascripts spéficiques au bundle -->
    <script type="text/javascript" src="/bundles/kitpagesshop/js/my.js"></script>

    {% block kitpages_shop_body %}
    {% endblock %}
{% endblock %}

Ensuite les layouts du bundle seront toujours structurés de cette façon :

{% extends "KitpagesShopBundle::layout.html.twig" %}

{% block kitpages_shop_body %}

    <!-- le contenu html -->

{% endblock %}

Si l'application utilise le template app/Resources/view/base.html.twig, potentiellement, le développeur n'a aucun layout à surcharger.

S'il veut intégrer des élements supplémentaires dans l'organisation de sa page, il aura la plupart du temps besoin de n'étendre dans app/Resources/KitpagesShopBundle que le fichier layout.html.twig

Les évènements

Si on fait un traitement dans le bundle pour lequel on pense que l'utilisateur pourrait vouloir compléter ou modifier le comportement depuis son application, une solution est de mettre en place des évènements.

Concrêtement, autour du traitements, on lancera 2 évènements, un avant le traitement et un après. L'évènement lancé avant le traitement permet éventuellement de désactiver le traitement à l'aide d'une fonction preventDefault().

Concrêtement nous allons ajouter quelques fonctions pratiques à un évènement de base :

Le fichier ShopEvent.php

<?php
namespace Kitpages\ShopBundle\Event;

use Symfony\Component\EventDispatcher\Event;

class ShopEvent extends Event
{
    protected $data = array();
    protected $isDefaultPrevented = false;
    protected $isPropagationStopped = false;
    
    public function preventDefault()
    {
        $this->isDefaultPrevented = true;
    }
    
    public function isDefaultPrevented()
    {
        return $this->isDefaultPrevented;
    }

    public function stopPropagation()
    {
        $this->isPropagationStopped = true;
    }

    public function isPropagationStopped()
    {
        return $this->isPropagationStopped;
    }
    
    public function set($key, $val)
    {
        $this->data[$key] = $val;
    }
    
    public function get($key)
    {
        if (!array_key_exists($key, $this->data)) {
            return null;
        }
        return $this->data[$key];
    }
}

Exemple d'utilisation d'un évènement

<?php

$event = new ShopEvent();
$event->set($transaction);
$this->dispatcher->dispatch(KitpagesShopEvents::ON_ORDER_PAYED, $event);

if (! $event->isDefaultPrevented()) {
    $this->myMethodThatDoesSomething($transaction);
}
$this->dispatcher->dispatch(KitpagesShopEvents::AFTER_ORDER_PAYED, $event);

Et dans un listener, il faut regarder si un autre évènement n'a pas interdit la propagation de l'évènement. Donc toutes les méthodes listener devrait commencer par le code suivant :

<?php

    /**
     * Event listener that send an email
     * @param ShopEvent $event
     */
    public function afterOrderPayedEvent(ShopEvent $event)
    {
        if ($event->isPropagationStopped()) {
            return;
        }
        // reste du traitement
    }

Remplacement d'un service dans un bundle

On est dans le fonctionnement de base de symfony2.

Il faut bien penser à mettre la classe de son service en paramètre afin qu'on puisse la changer directement dans le config.yml en changeant le paramètre.

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="kitpages_shop.cartManager.class">Kitpages\ShopBundle\Model\Cart\CartManager</parameter>
    </parameters>

    <services>
        <service id="kitpages_shop.cartManager" class="%kitpages_shop.cartManager.class%">
            <argument type="service" id="session" />
        </service>
    </services>

</container>

Surcharger un bundle

Que dire ? on peut à peu près tout faire. Il faut en revanche comprendre le fonctionnement du bundle. Je vous laisse vous reporter à la doc symfony ou à la doc du FOSUserBundle qui donne un exemple concrêt de surcharge

Conclusion

Aucune de ces recettes n'est universelle. Ca ne correspond pas à tous les besoins.

Cela dit on a parfois l'impression que c'est un sport de multiplier les fichiers et les répertoires dans les codes Symfony2. Cet article montre d'autres pistes d'extension que la surcharge du bundle.

Commentaires

Ajouter un commentaire