mercredi 7 mars 2012

PHP 5.4 : tirer un trait sur le passé ?


Le jeu de mot est un peu facile, je vous le concède. Mais comment ne pas saluer avec enthousiasme la sortie de PHP 5.4 (le 01/03/2012), et de sa plus grosse nouveauté : les traits. Ce concept peu connu dans l'univers PHP, mais qui ne l'est finalement guère plus dans d'autres langages courants va sans doute permettre de repenser en grande partie la conception objet avec PHP dans les mois à venir.

PHP 5.4, une version intermédiaire ou majeure ?

Avant de parler plus concrètement de l'implémentation des traits en PHP, sujet principal de ce billet, je voudrais revenir sur son titre. Si je considère PHP 5.4 comme une évolution majeure du langage, c'est parce qu'enfin des traits ont été tirés (bis repetita...) sur des caractéristiques archaïques du langage, de sorte que certaines des plus mauvaises habitudes de PHP ne sont plus seulement découragées, mais purement et simplement interdites. 

Si je ne devais prendre qu'un seul exemple, je citerais la suppression de la directive "register_globals" (et de son pendant programmatique import_request_variables()). Ce fameux register_globals, qui automatise l'intialisation de variables globales dans le script d'après les paramètres GET, POST, le contenu des cookies ou encore la session, tout ça dans le désordre le plus complet, est un symbole de la permissivité de PHP. Un symbole très négatif, car l'activation de cette fonctionnalité a conduit en son temps à de nombreux effets de bords et trous de sécurité (notamment combiné à d'autres mauvaises pratiques, comme l'utilisation de variables non-initialisées). Et pourtant, bien que découragé par sa désactivation par défaut depuis PHP 4.1 (!), l'usage du register_globals s'est maintenu pendant de (trop) nombreuses années.

Donc, la rénovation de PHP est en marche ! Il ne s'agit plus de bricoler par-dessus une carcasse vieillissante, mais de véritablement tailler dans la masse, d'amputer les parties malsaines pour améliorer substantiellement le langage.  
Pour résumer, PHP 5.4 est la bonne nouvelle que nous attendions depuis l'abandon du développement de PHP 6. Et même si la timide numérotation des deux dernières versions principales de PHP (5.3 et 5.4) ne le souligne pas suffisamment, ce sont bel et bien des versions majeures, et à ce titre devraient inciter les développeurs à réviser leurs habitudes et leur façon de faire, pour y intégrer ce que PHP leur offre désormais.

Et à ce propos, revenons justement aux traits ! Pour ceux qui parmi vous se sont intéressé tant soit peu au sujet et se sont peut-être interrogé sur l'intérêt concret de cette nouveauté, et pour les autres, ceux qui ne savaient pas que ça existait, et par conséquent ne savent probablement pas ce que c'est, je vous propose d'illustrer leur usage en examinant comment les traits auraient pu améliorer sensiblement la qualité du code de Zend Framework (v1) en se basant sur un exemple précis : la gestion d'options.

Mais avant d'étudier l'impact qu'aurait eu la disponibilité des traits sur le code de Zend Framework, il convient de revenir sur la définition même des traits. De quoi s'agit-il au juste ? Les traits peuvent dans une certaines mesure être comparés aux classes abstraites (impossibilité de les instancier, déclaration de propriétés, écriture de méthodes concrètes), mais avec deux différences notables : d'une part les traits ne sont pas hérités, mais simplement utilisés (mot-clé use), et d'autre part il est possible d'utiliser plusieurs traits dans une même classe concrète (comme si les classes abstraites autorisaient l'héritage multiple).

Pas d'héritage

Le fait que les traits ne nécessitent pas d'héritage permet de maintenir une hiérarchie d'objet qui ne soit pas détournée dans le simple but de la réutilisabilité du code. On décide de les utiliser sur une ou plusieurs classes selon que les méthodes qu'ils exposent seront utiles ou non à cette classe, indépendamment de la nature même des classes qui les utiliseront. De fait, il devient très facile de partager des fonctionnalités identiques entre plusieurs classes sans que celles-ci n'aient le moindre rapport du point de vue conceptuel. A ce titre, les traits sont aisément comparables aux helpers que l'on trouve dans certains frameworks.

Usage de multiples traits

L'absence de lien entre les traits et le mécanisme d'héritage a également facilité la possibilité d'agréger plusieurs traits sur une même classe. Pour PHP, les traits sont considérés comme des fragments de classes (méthodes et propriétés) dont on peut se servir comme briques de base pour fabriquer d'autres classes. De ce fait, le langage offre une grande liberté d'utilisation des méthodes exposées par les traits, en permettant notamment d'en modifier le nom et la visibilité au moment de l'intégration dans une classe concrète (aliasing - mot-clé as), et même de remplacer au run-time une méthode exposée par un trait par une autre méthode, issue d'un autre trait (mot-clé insteadof). 

Attention toutefois à cette souplesse, car si elle peut rendre de grands services, elle peut aussi conduire à des pratiques à la limite de l'obfuscation de code ! Je pense notamment à l'aliasing et à l'usage d'insteadof naturellement.

Comment traiter le cas de la gestion des options dans Zend Framework

De nombreux composants de Zend Framework proposent une gestion d'options, passées sous forme de tableau associatif à l'instanciation, pour faciliter la configuration des instances. Et je trouve ça fort pratique, à tel point qu'il m'arrive régulièrement de recommander de s'en inspirer pour des classes "maison". Mais en même temps, ça fait longtemps que je déplore la multiplicité d'implémentation de ces méthodes de gestion des options. En effet, de même que nous devons écrire nos propres méthode setOptions(), getOption(), etc. dans nos classes propriétaires, on peut constater non sans surprise qu'il existe pas moins de 33 implémentations d'une méthode qui s'appelle setOptions(), dont les principes et prototypes sont très proches sans être identiques.

L'usage d'un trait dans ce cas aurait apporté des bénéfices significatifs à deux populations :

 - aux développeurs du Zend Framework, en leur épargnant l'écriture de 32 méthodes superflues (sans parler des tests unitaires associés !), et en favorisant une certaine standardisation de la gestion d'options. Cette gestion étant réimplémentée dans chaque classe en ayant besoin, et pas toujours par le même contributeur, elle subit les influences des différents auteurs, avec au final des dérives assez importantes dans l'approche.

 - aux développeurs PHP utilisant Zend Framework dans leurs développements, car ils sont les premières victimes du manque de standardisation évoqué dans le point précédent. En effet, chaque fois que l'on trouve une méthode setOptions() dans Zend Framework, même si le rôle de ces différentes méthodes est similaire, il faudra relire la documentation, voire le code source, pour s'assurer du fonctionnement de chacune. Il ne sera pas possible de capitaliser sur la maîtrise "du mécanisme de gestion d'options de Zend Framework" car dans la réalité, il y a jusqu'à 33 mécanismes de gestion d'options différents !

Pour finir, voici en quelques lignes ce qui aurait changé dans le code de Zend Framework si l'on avait pu utiliser les traits au moment de son développement. L'exemple est volontairement basé sur une implémentation minimale de setOptions(), celle de Zend_Navigation_Page, pour des soucis de lisibilité :

Version "officielle" :

abstract class Zend_Navigation_Page extends Zend_Navigation_Container
  /* ... */
  public function setOptions(array $options)
      {
          foreach ($options as $key => $value) {
              $this->set($key, $value);
          }
          return $this;
      }
  /* ... */
}


Version "traitée" en PHP 5.4

trait OptionsHandler {
  public function setOptions(array $options)
      {
          foreach ($options as $key => $value) {
              $this->set($key, $value);
          }
          return $this;
      }
}

abstract class Zend_Navigation_Page extends Zend_Navigation_Container
    use OptionsHandler;
  /* ... */
}

Ces deux écritures sont strictement identiques du point de vue du run-time et de l'appel de la méthode setOptions(). Simplement, en déportant la méthode dans un trait, on lui a assuré une réutilisabilité totale.

Quand on sait qu'il y a des classes aussi différentes que Zend_Translate_Adapter, Zend_Validate_Ip ou encore Zend_Tag_Cloud qui chacune définit une méthode setOptions(), il et évident qu'il aurait été absurde d'un point de vue conceptuel de les faire dériver d'un ancêtre commun, ce qui explique le choix de réimplémenter systématiquement une méthode qui aurait pourtant pu être rendue générique et utilisable par tous ces composants. 

Les traits sont la seule solution à la fois élégante (un minimum de code), sûre (contrôles à la compilation) et performante (contrairement aux helpers, qui utilisent la méthode magique __call, singulièrement lente) de gérer ce problème de conception.

Il me semble que le temps est venu de tirer également un trait sur l'argument de ses détracteurs selon lequel PHP n'est pas pourvu d'un modèle objet digne de ce nom !