L'occasion s'est cependant présenté il y a peu dans le cadre du développement d'Atoum, puisque j'ai eu besoin d'utiliser le patron de conceptionsingleton, pour lequel l'utilisation du LSB est particulièrement adaptée.

Avant PHP 5.3, l'implémentation naïve d'un singleton ne fonctionnait pas comme attendu :

<?php

abstract class singleton {
   public static function getInstance()
   {
      static $instance = null;
      if ($instance === null)
      {
         $instance = new self();
      }
      return $instance;  
   }
   protected function __construct() {}
   protected function __clone() {}
}
class foo extends singleton {}

$foo = foo::getInstance();
?>

L'exécution de ce code génère en effet l'erreur suivante :

PHP Fatal error:  Cannot instantiate abstract class singleton in /usr/home/fch/tmp/singleton.php on line...

La raison de cette erreur est très simple.

En effet, avant la version 5.3, PHP est totalement incapable de savoir que c'est la classe foo qui a appelé la méthode singleton::getInstance(), puisque le mot-clef self se réfère à la classe qui l'appelle, en l’occurrence singleton.

Cette dernière étant définie comme abstraite et ne pouvant donc être instanciée, le langage n'a pas d'autre choix que de nous renvoyer une erreur.

La seule solution pour obtenir le résultat attendu était alors de dupliquer le code de la méthode singleton::getInstance() dans chacune des classes implémentant le patron de conception, ce qui complètement contraire au bonne pratique de développement, ou bien d'autres astuces tout aussi horrible à mon sens.

Heureusement, les développeurs de PHP ont pris (il a fallu le temps, mais c'est un autre débat) conscience du problème, et avec la version 5.3 est arrivé le LSB (et non le LSD).

Pour résumé très grossièrement mais très efficacement, le LSB permet de faire de la résolution dynamique d'appel statique, via l'introduction d'un nouveau mot-clef, static, et de deux fonctions, get_called_class() et forward_static_call().

Ainsi outillé, le langage est maintenant capable de savoir que l'appel à la méthode singleton::getInstance() se fait via la classe foo, et il est donc possible d'instancier la classe foo au lieu d'essayer d'instancier la classe singleton.

Pour cela, il suffit de modifier légèrement le code de notre classe singleton :

<?php
   ...
   public static function getInstance()
   {
      static $instance = null;
      if ($instance === null)
      {
         $instance = new static();
      }
      return $instance;  
   }
  ...
?>

Suite aux commentaires que j'ai reçu au sujet de ce billet, Je précise que ce code fonctionne absolument parfaitement, malgré le fait que la variable statique définie dans la classe singleton::getInstance() est censée avoir une portée locale à la méthode.

Cependant, PHP étant ce qu'il est, cette solution n'est pas parfaite, puisque dans un cas particulier (que j'ai mis du temps à trouver, malgré la conséquente couverture de tests unitaires d'Atoum), elle ne fonctionne tout simplement pas.

Le code suivant illustre le problème :

<?php
...
class a extends singleton {}

var_dump(a::getInstance()); // object(a)#1 (0) {}
$code = 'class b extends a {}';
eval($code);  // je sais, c'est moche, mais c'est ainsi.

var_dump(b::getInstance()); // object(a)#1 (0) {} ... ?>

L'appel à la fonction eval() perturbe complètement le fonctionnement du LSB, pour une raison inconnue, et ce dernier se retrouve incapable de résoudre l'appel statique correctement.

Renseignement pris, le problème vient de l'utilisation de eval(), qui décale la définition de la classe au sein de PHP lors de l'exécution et non à la compilation, ce qui fait que les variables statiques ont déjà une valeur.

Il faut d'ailleurs noter que le code suivante pose exactement le même problème, pour ceux qui me dirais que de toute façon, eval() est à proscrire :

<?php
...
class a extends singleton {
}
var_dump(a::getInstance());  // object(a)#1 (0) {}
if (1 > 0)
{
  class b extends a {}
}

var_dump(b::getInstance()); // object(a)#1 (0) {} ... ?>

J'ai donc fait un rapport de bug, mais j'ai comme un doute sur le fait qu'il soit un jour résolu.

Heureusement, il y a une solution, qui consiste à utiliser un tableau comme variable statique dans la méthode singleton::getInstance(), et non une simple variable, et d'utiliser la fonction get_called_class() de la manière suivante :

<?php
   ...
   public static function getInstance()
   {
      static $instances = array();
      $class = get_called_class();

      if (isset($instances[$class]) === false)
      {
         $instances[$class] = new $class();
      }
      return $instances[$class];  
   }
  ...
?>

Ainsi, la classe singleton est fonctionnelle quelque soit le contexte de mise en œuvre.

Conclusion, les variables statiques et la définition de classe à l'exécution ne font pas bon ménage, et cela peut induire des bugs très difficile à localiser, et il faut donc avoir cela en tête si d'aventure le besoin de réaliser ce genre de choses se fait sentir.

Et non, au risque de vous décevoir, je ne dirais pas une certaine citation à la Jean-Pierre Coffe, même si ce n'est pas l'envie qui m'en manque.