J'ai noté un comportement sympa de PHP, mais c'est tout à fait logique. Je voulais vous en faire part car je me suis cassé la tête un p'tit moment avant de trouver. Attention, c'est très spécifique mais ça m'arrive d'écrire ce genre de chose et je ne pense pas être le seul
Alors : on a 2 classes par exemple. La première classe A qui est un singleton. Donc on retrouve notre méthode statique getInstance, et notre constructeur est bien sûr privé. La seconde classe B est en association (agrégation et pas composition) avec A, c'est à dire que A peut utiliser B et inversement.
Voici ce que j'avais fait : on appelle A::getInstance, et A::getInstance va appeler A::__construct. Jusque là, rien de choquant. Dans A::__construct, j'appelais B::uneMethode, et dedans, j'appelais A::getInstance pour récupérer des paramètres sur A.
Grosso modo, on a le code suivant (j'ajoute des var_dump dans getInstance pour la suite du sujet) :
class A {
private static $_instance = null;
private function __construct ( $parameter = true ) {
// …
if(false === $parameter)
B::uneMethode();
// …
}
public static function getInstance ( $parameter = true ) {
var_dump(null === self::$_instance);
if(null === self::$_instance)
self::$_instance = new self($parameter);
var_dump(null === self::$_instance);
return self::$_instance;
}
// …
}
class B {
// je la mets statique pour aller plus vite, mais ça ne change rien.
public static function uneMethode ( ) {
// …
A::getInstance();//->getParameters();
// …
}
}
A::getInstance(false); On aura en sortie : Code : Tout sélectionner
bool(true)
bool(true)
bool(false)
bool(false)On demande l'instance de A. Donc l'instance est nulle au départ, donc true. On demande l'instance (new self($parameter);), donc on entre dans le constructeur. Ce dernier, va appeler B::uneMethode. Cette méthode va appeler A::getInstance(). On retombe donc sur le premier var_dump. Est-ce que l'instance existe ? Non toujours pas … enfin … en réalité si, elle existe, mais elle n'est pas assignée à la variable self::$_instance, donc le test retourne true. Donc on retourne encore une fois dans le constructeur (car new self est encore appelée), mais on n'appelle plus B::uneMethode car le paramètre par défaut est utilisé. (Notez ici que l'on pourrait plonger dans une récursivité assez méchante. Ce n'était pas mon cas, je constatais seulement le double appel de la méthode __construct). Donc on ne retourne pas dans B. On quitte le constructeur, on revient dans getInstance, on remonte, on assigne une première fois, on remonte, on assigne une seconde fois … et non ! Car la première assignation a été faite, donc on n'assigne pas la deuxième (à cause de notre test de nullité de l'instance). Donc on se retrouve avec une instance ayant les paramètres par défaut (si on enregistre les paramètres).
Mais alors comment lancer l'appel à B ? Il suffit de déplacer l'appel. On le supprime du constructeur et on le place dans A::getInstance de cette façon :
public static function getInstance ( $parameter = true ) {
var_dump(null === self::$_instance);
if(null === self::$_instance) {
self::$_instance = new self($parameter);
if(false === $parameter)
B::uneMethode();
}
var_dump(null === self::$_instance);
return self::$_instance;
}
C'est un cas particulier que j'ai simplifié, mais ça peut très facilement arriver. Soit on n'a pas de paramètre et on boucle (récursivité sur le new), soit on se retrouve avec une instance paramétrée par défaut.Ça fait intervenir plusieurs phénomènes, mais c'est principalement une question de statisme et d'assignation. PHP va d'abord évaluer new self($parameter); avant de l'assigner à self::$_instance ce qui est très logique et normal. Je ne fais aucun reproche à PHP, qu'on soit d'accord.
Moralité, attention quand vous coder des choses un peu particulière a bien tester, mais avant tout : bien réfléchir au comportement du langage, à sa façon d'interpréter les choses, de comprendre les choses, et l'ordre d'évaluation (ce dont il est question ici).
J'ai mis du temps à trouver car je travaillais sur 3 fichiers différents … Donc penser à bien atomiser vos tests si c'est possible (dans mon cas — pas celui de l'exemple —, ça n'était pas possible par exemple).
Ah oui, pourquoi ne pas passer les paramètres à B::uneMethode en argument, et comme ça, B::uneMethode n'appelle pas A::getInstance ? C'est une solution qui évite tout ces problèmes, mais elle ne me convenait pas on peut utiliser directement B::uneMethode sans passer par A. Et donc, dans ce cas, on doit utiliser les paramètres par défaut. Voilà le pourquoi du comment
Fin de la séance mal de crâne