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
.
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.
10 réactions
1 De metagoto - 25/10/2010, 22:58
Bug?
Tu ne penses pas que le "bug" est dû au fait qu'il n'y a qu'une seule variable statique $instance dans la méthode statique getInstance... bref, la variable a un storage statique. Il n'y a qu'un seul storage pour toute la hiérarchie. Le premier a::getInstance() va assigner la variable une fois pour toute. Si ça fonctionnait comme tu l'imagines (le souhaites), là je pense qu'il y aurait effectivement un sérieux bug!
Le comportement actuel me parait tout à fait normal :D
2 De mageekguy - 25/10/2010, 23:00
@metagoto : Sauf que si tu défini la classe b , sans passer par eval() ou via une condition, et donc à la compilation et non au runtime, ça fonctionne parfaitement.
Alors, effectivement, suivant le point de vue, tu as raison et j'ai tort (et pour dire la vérité, je suis de ton avis, ma solution ne devrait pas marcher dans l'absolu, mais tout dépend de la façon dont PHP gère l'héritage), mais quelque soit le point de vue, dans l'absolu, je dirais
, car le fonctionnement de PHP n'est pas cohérent.Soit eval() ou n'importe quelle instruction qui déporte la définition de la classe au runtime génère le même comportement que si la classe dérivée était définie lors de la compilation, soit la classe dérivé définie à la compilation a le même comportement que lorsqu'elle est définie au runtime.
Mais pas un comportement dans un cas, et un différent dans l'autre.
3 De Jérémy - 25/10/2010, 23:08
Arrêtes moi si je me trompe, mais une variable static à une portée locale, ce qui signifie que dans ton exemple, elle est référencée au niveau de ta class "singleton" et que toutes les class qui dérivent de singleton partagent la même variable $instances. Il est donc "logique" que ton code échoue et que var_dump(b::getInstance()) retourne une instance de a.
4 De mageekguy - 25/10/2010, 23:12
@Jérémy : voir mon commentaire à metagoto. Donc oui je t'arrête et te dis que tu te trompes :).
Vu que ça fait deux personnes qui ne captent pas la subtilité, j'ai édité le billet en conséquence.
5 De metagoto - 25/10/2010, 23:55
Tu as raison, mageekguy, il y a un problème!
Je raisonnais comme si
$instance
était un membre statique (et pas statique dans la méthode, mais je pense que ça devrait être la même chose dans cet exemple).Je viens de tester, quand c'est un membre statique, ça se comporte comme attendu, on n'obtient qu'un seul objet, que la classe b soit déclarée dans une condition ou non.
Quand la variable est statique dans la fonction, ça
(comportement différent si on hérite de singleton ou a d'ailleurs).Je reste prudent, on ne sait jamais (comprendre, c'est peut être une feature!).
6 De Stéphane - 26/10/2010, 10:50
Ce n'est pas tout à faire le sujet mais il y a-t-il un intérêt à mettre la méthode __clone() en "protected" ? Je l'aurai plutôt déclarée en "final private".
En tout cas c'est comme cela que j'écris mon Singleton ^^
7 De mageekguy - 26/10/2010, 11:55
@Stéphane : Avec ta solution, tu verrouille vraiment, même une classe dérivée ne peut pas se servir de __clone(), ce qui pourrait tout de même être intéressant dans certain cas (mais je n'ai pas d'exemples probant sur l'instant).
C'est un choix.
8 De Steuf - 03/11/2010, 12:22
@Stéphane Pour répondre à cette question, un exemple probable du besoin de surclasser __clone dans une classe dans un singleton est le cas suivant :
- Bien entendu une classe avec d'autres instances de classes dans les propriétés (sinon __clone n'a aucun intérêt évidemment)
- Non pas avoir 1 unique instance de la classe dans le singleton, mais avoir plusieurs instances dans le singleton.
Dans ce cas de figure, nous n'avons pas 1 variables statiques avec une unique instance, mais une variable contenant un tableau d'instance. Il faut bien entendu que l'on ai envie de faire des clonages dans la méthode getInstance plutôt que des nouvelles instances de la classe.
J'avoue c'est un peu tordu, mais un cas probable. Je réserve le clonage aux collections d'objets pour ma part, bien plus rapide que de refaire 1 instance par ligne (qui instancie toutes les dépendances etc).
Sinon j'aime assez ce blog, je suis souvent en accord avec ce que dit l'auteur. Comme ici, nous avons "enfin" la possibilité de faire des résolution de portée avec "static".
9 De Mathieu - 18/11/2010, 09:05
@metagoto : En fait ça ne "déconne" pas, car PHP se contente de respecter ses propres spécifications sur les variables statiques de fonction. En clair, $instance n'est pas locale à getInstance(), mais bien à a::getInstance() ou b::getInstance().
Je pense que PHP identifie les méthodes en tenant compte de l'espace de nom dans lequel elles sont appelées. Mais du coup il est "curieux" que le LSB ne fonctionne pas avant PHP 5.3. (Car notez que, si on remplace le mot clé static par un nom de classe passé en argument à getInstance, ce Singleton fonctionne parfaitement en PHP 5.0.)
10 De mageekguy - 18/11/2010, 09:37
@Mathieu : Je t'assure que ça déconne, mais le problème provient de l'
eval()
, pas du LSB.