· Tech watch

Industrialisation de projets multi-sites

Dans le cadre de plusieurs projets ayant des problématiques multi-sites (déclinaisons par pays ou par client), nous avons été amenés à concevoir et mettre en place une organisation technique permettant d’industrialiser les développements.

Objectifs et contexte technique

Pour ces différents projets, les objectifs à atteindre étaient les suivants :
– Permettre une capitalisation maximum du code développé
– Assurer la cohérence entre les différentes versions du code capitalisé et spécifique
– Sécuriser les déploiements et la maintenance des différents sites

Cet article est né du retour d’expérience de projets multi-sites réalisés à l’aide du framework symfony et utilisant le gestionnaire de sources Subversion . Nous avons tiré partie des spécificités techniques offertes par symfony et Subversion mais au delà des aspects spécifiques de ces outils, les principes exposés sont bien entendu applicables à d’autres outils.

Capitalisation du code

Grâce au mécanisme de plugins de symfony, nous avons choisi de regrouper tout le code fonctionnel sous forme de plugins pour pouvoir le réutiliser plus facilement. Chaque déclinaison du site est un projet symfony à part entière, avec sa ou ses propres applications et incluant les plugins développés.

Les plugins symfony ont la particularité de pouvoir être surchargés dans chaque application avec une granularité relativement fine. Par exemple, il est facile de ne surcharger qu’une action, qu’un template ou qu’un partiel.

Afin de pouvoir tirer parti de cette finesse de surcharge, il est important d’implémenter le code de manière « surchargeable ». Cela se traduit principalement par le découpage de chaque classe « classique » en deux :
– Une classe de base qui contient le code effectif. Cette classe est par convention préfixée par « Base… »
– Une classe qui hérite de la classe « Base… », et qui ne constitue au niveau du plugin qu’une coquille vide

Pour permettre la surcharge la plus souple possible, ce découpage des classes doit être généralisé au sein des plugins (actions, components, model, utilitaires,… ). Il s’agit de la méthode utilisée pour les plugins publics développés pour symfony, et du code généré par Propel ou Doctrine. Pour simplifier la création de modules dans les plugins suivant cette convention, on peut gagner du temps en utilisant le plugin sfTaskExtraPlugin et la tâche generate:plugin-module, qui pré-générera les classes d’actions.

Si un besoin de surcharge d’une classe est nécessaire dans une application particulière, il suffit de redéfinir la « coquille vide » (qui hérite de la classe qui contient le code de base), et de surcharger les méthodes qui doivent l’être. Le système d’autoloading de symfony va faire le reste.


<?php
// plugins/myPlugin/modules/contact/actions/actions.class.php
require_once dirname(__FILE__).'/../lib/BasecontactActions.class.php' ;

class contactActions extends BasecontactActions
<em>
</em>

// plugins/myPlugin/modules/contact/lib/BaseContactActions.class.php
class BasecontactActions extends sfActions
<em>public function executeList(sfWebRequest $request)
{
...
</em>
}

// apps/frontend/modules/contact/actions/actions.class.php
class contactActions extends BasecontactActions
<em>public function executeList(sfWebRequest $request)
{
parent ::executeList() ;
$this->doSomethingMore() ;
</em>
}

Pour les templates / partial, il suffit de redéfinir le fichier concerné avec le même nom dans l’application pour qu’il soit appelé à la place du plugin. Afin d’obtenir une bonne granularité de surcharge pour ces éléments, les templates du plugin doivent être bien découpés et des « components » doivent être créés dès que nécessaire. Il sera ainsi plus facile de ne surcharger qu’un élément de l’interface et de réutiliser des éléments dans de nouveaux templates.

Autre particularité intéressante de symfony, on peut également surcharger un plugin depuis un autre plugin, en jouant sur leur ordre de chargement. Il suffit pour cela d’ordonner correctement les plugins lors de l’appel à enablePlugins() dans la classe ProjectConfiguration. Ainsi, il est possible par exemple de surcharger un plugin tiers par un de nos plugins commun (pris en compte pour tous nos sites), et qui sera lui même surchargeable au niveau de l’application si besoin (pour chaque site).

Organisation des sources

Afin de bénéficier facilement des plugins développés dans les différentes applications, nous utilisons une fonctionnalité du gestionnaire de source : la propriété « svn:externals » ([http://svnbook.red-bean.com/nightly/en/svn.advanced.externals.html]). Cette propriété permet de faire référence à un autre endroit du dépôt ou à un dépôt externe qui sera automatiquement remonté lors du checkout et des updates. On a donc pour chaque déclinaison de site, une application qui référence via des « svn:externals » les plugins communs. Chaque déclinaison peut surcharger leurs fonctionnalités dans les répertoires qui leurs sont propres.

Afin de pouvoir garantir la cohérence entre les différentes versions et sécuriser déploiement, maintenance et montées de version, les conventions branches et tags classiques de Subversion peuvent être appliquées. Ces conventions doivent être utilisées à la fois pour les plugins et pour les applications. Il suffit alors de faire pointer les svn:externals vers des branches/tags particuliers pour s’assurer de la maîtrise de l’application.

Nous avons baptisé cette procédure le « freeze » des externals. Elle consiste, lorsqu’on tagge une application, à modifier les références dans les externals afin de ne plus remonter les mises à jours de plugins lors de futurs « svn up ». Deux méthodes sont envisageables :
– modification des chemins de « svn:externals » pour pointer vers le tag correspondant
– « svn copy » des plugins internes vers le tags de notre application. On obtient une structure cohérente avec tout le code intégré et on bénéficie de l’historique des plugins grâce au svn copy. Les plugins externes peuvent être rapatriés par un « svn export ».

Voici un exemple de la première solution :

svn:externals de siteA/trunk/plugins :
monPlugin http://mon-depot-svn/core/trunk/monPlugin

svn:externals de siteA/branches/1/plugins :
monPlugin http://mon-depot-svn/core/branches/1/monPlugin

svn:externals de siteA/tags/1.1/plugins :
monPlugin http://mon-depot-svn/core/tags/1.1/monPlugin

Il est relativement simple d’écrire un script shell qui permettra de gagner du temps en « freezant » automatiquement les externals en une seule ligne de commande.

Avec ce mécanisme, on peut utiliser classiquement les tags/branches lors des déploiements et mises à jour sur les différents environnements (développement, pré-production, production). Chaque environnement se retrouve donc sécurisé (pas d’update intempestif des externals,…)

Voici une illustration synthétique de cette organisation (les liens « Site B » vers « Core » ne sont pas représentés pour alléger le schéma) :

Phasage projet

Dans ce genre de projet, toutes les déclinaisons de site ne sont jamais spécifiées dès le départ. Il n’est donc pas possible, au démarrage, de déterminer les fonctionnalités qui vont être communes ou spécifiques. C’est la première déclinaison qui va servir de « version de référence ». Même si on essaie de bien découper le code dès la première déclinaison, un travail de refactoring est indispensable lors de la deuxième pour re-découper correctement les fonctionnalités. Au fur et à mesure des nouvelles déclinaisons, ce travail de refactoring sera de plus en plus léger.

Avec plusieurs déclinaisons, va également apparaître un nouveau travail important lors des corrections ou ajouts de fonctionnalités. En effet, à chaque modification du code, il faut décider si elle doit être intégrée au « pot commun » ou si elle doit rester dans une application spécifique. En plus du travail habituel de « merge » (répercussion d’une correction du trunk vers une branche), si le code doit être répercuté dans le pot commun, un travail d’adaptation et de validation supplémentaire est nécessaire sur les différentes déclinaisons du site.

Une très bonne compréhension de l’organisation des sources par tous les développeurs est nécessaire et un suivi assez fin par un référent est également indispensable, pour décider de l’intérêt ou non de répercuter les modifications dans les différentes branches.

Bilan

La réussite de cette organisation repose sur différents facteurs.

Tout d’abord, la qualité initiale du code qui servira de référence est essentielle. Si elle n’est pas assurée, elle entraînera des problèmes à plus ou moins long terme.

Dans le même registre, le soin apporté au découpage des fonctionnalités est important. Obtenir une bonne granularité de surcharge permettra une meilleure réutilisation et un simplification des futures déclinaisons. L’impact sur les charges initiales est non-négligeable, mais ce surcoût est à considérer comme un investissement qui sera rentabilisé au fur et à mesure des déclinaisons du site.

Enfin, la maintenance, même si elle va être moins coûteuse que si il n’y avait aucune capitalisation de code, impose une bonne définition des processus (identification, correction, validation) et un contrôle assez pointu pour éviter le plus possible les régressions.

Un commentaire

  1. Merci pour cet article.
    Mais sauf erreur de ma part, pour redéfinir une action d’un plugin, il convient d’inclure la librairie à la main (l’autoloading ne fonctionne pas) :

    // apps/frontend/modules/contact/actions/actions.class.php

    require_once(sfConfig::get(‘sf_plugins_dir’).’/myPlugin/modules/contact/lib/BasecontactActions.class.php’);

    class contactActions extends BasecontactActions
    {
    public function executeList(sfWebRequest $request)
    {
    parent::executeList();
    $this->doSomethingMore();
    }
    }

Les commentaires sont désormais fermés.