Tendances

produire-du-code-extensible-avec-symfony

Produire du code extensible avec Symfony

Lors de la réalisation d’applications complexes, la difficulté principale rencontrée sur le long terme est l’augmentation de la dette technique. Ce symptome s’explique la plupart du temps par la conception du code qui ne s’avère pas suffisamment souple pour supporter les évolutions futures de l’application. Il est alors nécessaire de développer des rustines voire de réécrire certains pans de l’application. Il est donc important d’appliquer des principes de conceptions éprouvés et permettant une évolution sereine de projets dans le temps.

1. Principe Open/Closed

Le principe Open/Closed est le second des cinq principes SOLID décrit par Robert C. Martin pour guider la production de code de qualité en programmation orientée objet. Ce principe demande de développer des classes qui soient ouvertes à l’extension, fermées à la modification. Cela signifie tout simplement que le code que l’on produit doit pouvoir être augmenté de nouvelles fonctionnalités par l’ajout de nouvelles classes (il est ouvert à l’extension), sans qu’aucune modification du code existant ne soit requise (il est donc fermé à la modification).

Un package qui obéit à ce principe peut donc être distribué à la communauté avec un ensemble de fonctionnalités standards pour les cas d’usage les plus courants. Ces fonctionnalités peuvent ensuite être facilement étendues pour les besoins d’un projet spécifique, sans pour autant nécessité l’intervention des personnes en charge du maintien du package.

2. Le design pattern Strategy

Le design pattern Strategy est un moyen simple et rapide de mettre en place des fonctionnalités répondant au principe Open/Close. Dans ce pattern, le principe est de faire en sorte que l’implémentation d’une fonctionnalité ne soit pas statique mais dynamique. Concrètement, suivant le contexte courant de l’action, le traitement correspondant pourra suivre un chemin d’exécution différent.

Tout le principe est de s’appuyer sur l’abstraction de l’objet responsable du traitement : une interface définit une API permettant de normer les méthodes disponibles. Les détails de l’implémentation sont laissés à des classes dédiées dont le but est de fournir des traitements spécifiques à un cas d’utilisation donné.

Le choix de la classe adaptée à un contexte donné est ensuite délégué à un dispatcher qui est le seul à connaitre l’ensemble des implémentations existantes et qui est à même de désigner celle qui saura répondre à un besoin ciblé. Ainsi, si l’on veut étendre le fonctionnement, il suffit d’ajouter une nouvelle classe implémentant l’interface précédente dans le dispatcher, et les requêtes la concernant lui seront automatiquement transmises par ce dernier.

3. Mise en place dans Symfony

Une première solution pour mettre en place un design pattern Strategy dans Symfony serait de:

  • déclarer les différentes stratégies

  • déclarer le dispatcher

  • Ajouter les différentes stratégies au dispatcher

La configuration YAML est la suivante :

# services.yml
services:
    foo.strategy
        class: FooStrategy
    bar.strategy:
        class: BarStrategy

    baz.dispatcher:
        class: BazDispatcher
        calls:
            - [ ‘addStrategy’, [ @foo.strategy ] ]
            - [ ‘addStrategy’, [ @bar.strategy ] ]

Nous créons les stratégies Foo et Bar, puis le dispatcher Baz auquel nous injectons les deux stratégies.

L’inconvénient de cette configuration est visible lorsque l’on souhaite ajouter une nouvelle stratégie au dispatcher. Il devient en effet nécessaire de modifier la définition du service afin de lui ajouter un appel supplémentaire à la méthode addStrategy(). Même s’il est possible de le faire sans modifier ce fichier de configuration (utilisation de CompilerPass, réécriture de la définition dans un autre fichier), il existe un moyen plus souple d’obtenir le même résultat dans Symfony : les tags.

Ce principe largement utilisé dans Symfony2 au niveau des composants (formulaires, templating, event dispatcher, etc.) permet spécifiquement de détecter des services conçus pour une certains utilisation sans modifier ledit code de détection.

Pour cela, il faut ajouter une compiler pass, un traitement métier que Symfony exécute durant la compilation de son kernel et la création de son cache. Cette classe pourra donc récupérer l’ensemble des services ayant recu un tag particulier, puis les injecter dans le dispatcher.

Nous aurons donc le code de configuration suivant :

# services.yml
services:
    foo.strategy:
        class: FooStrategy
        tags:
            - { name: baz.strategy }
    bar.strategy:
        class: BarStrategy
        tags:
            - { name: baz.strategy }

    baz.dispatcher:
        class: BazDispatcher

Et un code de compilation ressemblant à :

$manager = $container->getDefinition(‘baz.dispatcher’);
$strategies = $container->findTaggedServiceIds(‘baz.strategy’);
foreach ($strategies as $id => $attributes) {
    $manager->addMethodCall('addStrategy', array(new Reference($id)));
}

Il est maintenant possible de rajouter une stratégie sans modifier le code de configuration défini dans notre Bundle, ce qui donne toute latitude aux utilisateurs pour ajouter de nouvelles fonctionnalités.

4. Cas d’usage dans Open Orchestra

Afin de rendre la plateforme Open Orchestra la plus modulaire possible, toutes les entrées dans le menu de navigation du backoffice sont décrites sous la forme de stratégies. Cela permet à des modules externes d’ajouter une entrée dans le panneau de navigation sans pour autant faire de modifications dans le code HTML de la page ni dans le code PHP d’Open Orchestra.

Dans ce contexte, le rôle du dispatcher est joué par le NavigationPanelManager, lequel stocke un tableau d’objets de type NavigationPanelInterface. Celle-ci permet de décrire une entrée du menu avec toutes les informations nécessaires à son rendu (position, hiérarchie, etc.).

interface NavigationPanelInterface
{
   const EDITORIAL = 'editorial';
   const ADMINISTRATION = 'administration';

   public function setTemplating(EngineInterface $templating);
   public function show();
   public function getName();
   public function getParent();
   public function getWeight();
   public function getRole();
}

La classe NavigationPanelManager possède une méthode addStrategy() pour insérer une nouvelle entrée de navigation. On peut remarquer que le tableau de stratégies est multidimensionnel, ce qui permet de gérer correctement les aspects hiériarchiques du menu.

class NavigationPanelManager
{
    protected $templateEngine;

    //...

   public function addStrategy(NavigationPanelInterface $strategy)
    {
        $this->strategies[$strategy->getParent()][$strategy->getWeight()][$strategy->getName()] = $strategy;
        $strategy->setTemplating($this->templateEngine);
    }

    public function show()
    {
        return $this->templateEngine->render('...', array(
           'strategies' => $this->strategies
       ));
    }
}

Lors du rendu du panneau de navigation (appel à la méthode show() du dispatcher), les stratégies enregistrées dans le dispatcher sont envoyées dans le template Twig, qui peut itérer sur la collection et afficher l’ensemble des entrées enregistrées.

Le contenu (simplifié) du template ressemble donc à ceci:

<ul>
{% for sectionName, section in strategies %}
    <li>
        <a href=”#”>{{ sectionName }}</a> {# affichage du titre de section #}
        <ul>
        {% for weight in section %}
            {% for element in weight %}
                {% if is_granted(element.getRole) %}
                <li>{{ element.show|raw }}</li>
                {% endif %}
            {% endfor %}
        {% endfor %}
        </ul>
    </li>
{% endfor %}
</ul>

Ainsi, pour ajouter de nouvelles entrées au menu, il suffit de déclarer un service implémentant NavigationPanelInterface et de le tagger avec open_orchestra_backoffice.navigation_panel.strategy, Symfony se chargera du reste ! Prenons l’exemple de la page des thèmes, accessible via son entrée dans le menu :

# services.yml
open_orchestra_backoffice.navigation_panel.themes:
    class: %open_orchestra_backoffice.navigation_panel.administration.class%
    arguments:
        - themes
        - ROLE_ACCESS_THEME
        - 60
    tags:
        - { name: open_orchestra_backoffice.navigation_panel.strategy }

Open Orchestra a donc connaissance du service et l’utilisera pour afficher une entrée spécifique dans le menu.

5. Conclusion

La mise en place du design pattern Strategy pour respecter le principe Open/Closed permet à moindre coûts de faciliter l’évolution future d’une application. Dans le cas d’un projet Open-Source, c’est aussi la garantie de permettre aux utilisateurs des fonctionnalités métier au code existant sans pour autant nécessiter l’intervention du mainteneur ou de dénaturer le projet avec une implémentation trop spécifique. Avec de tels avantages, pourquoi s’en priver?

Auteurs
Julien Chichignoud LinkedIn
Nicolas Thal LinkedIn

Voir toutes les tendances