Depuis PHP5, j’entends souvent dire que les objets utilisent les références lors d’une assignation ou lorsqu’on les passe en paramètre par exemple. Même la documentation officielle (http://php.net/manual/fr/language.operators.assignment.php) l’explique noir sur blanc:
« Une exception au comportement d’assignation par valeur en PHP est le type object, ceux-ci sont assignés par référence dans PHP 5. La copie d’objet doit être explicitement demandée grâce au mot-clé clone. ».
C’est ce que l’on dit pour simplifier les choses, mais nous verrons plus loin que ce n’est pas tout à fait vrai.
En effet, je me suis penché sur la chose lors d’une mission où j’ai rejoint une équipe qui a dû passer une grosse application de PHP4 vers PHP5.3. A l’époque de PHP4, les objets étaient passés en copie, ce qui pouvait poser deux gros problèmes:
- Explosion de la mémoire
- Mise à jour d’une copie d’un objet en pensant travailler sur l’original.
Le « & » (pour « référence ») était donc vu comme le messie pour pallier à ce problème.
En php4 :
1 2 3 4 5 6 7 8 |
$monObjet = new stdClass() ; $monObjet->i = 1 ; Function add(&$obj){ //Le « & » est obligatoire pour que l’objet « original » soit également modifié $obj->i++ ; } add($monObjet) ; echo $monObjet->i ; //Affiche 2 |
Et en PHP5, le résultat est le même donc tout va bien ! Le « & » n’a donc, à priori, pas d’impact pour les objets.
1. Mon problème
Je vais rapidement illustrer un problème récurrent que j’ai pu observer lors de l’analyse du projet. J’ai synthétisé au maximum le code pour n’avoir que le strict minimum. Gardez également en tête que nous venions d’une application PHP4 qui nécessitait l’utilisation des références pour les objets.
Nous avons d’abord une classe « Tab », qui a un indice (int) :
1 2 3 4 5 6 7 8 9 10 11 12 |
class Tab{ private $indice; //Indice du Tab public function setIndice($indice) { $this->indice = $indice; } public function getIndice() { return $this->indice; } } |
Et un TabManager, qui gère des « Tab »:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class TabManager{ private $currentTab; //Le Tab « en cours » public function setCurrentTab (& $tab) { //Assigne le « tab en cours » $this->currentTab = & $tab; } public function getTabWithIndice($indice){ //Création d’un Tab, avec l’indice donné en paramètre. Ce tab sera indiqué comme le « tab en cours » $this->currentTab = new Tab(); $this->currentTab ->setIndice($indice); return $this->currentTab; } } |
Et maintenant l’exécution (simplifiée) en PHP5:
1 2 3 4 5 6 7 8 9 10 11 12 |
//Mon gestionnaire de « Tab » $tabManager = new TabManager(); //Mon premier « Tab », qui sera le « Tab en cours » $premierTab = new Tab(); $premierTab ->setIndice(1); $tabManager->setCurrentTab($premierTab); $deuxiemeTab = $tabManager->getTabWithIndice(2); //Récupération d’un deuxième « Tab ». //Je veux encore faire une opération sur mon premier « Tab » echo $premierTab->getIndice(); //Ho surprise... $premierTab->indice vaut … 2 ! |
Comme vous pouvez le constater, le premier « Tab » s’est transformé en deuxième « Tab » sans que l’on ait demandé quoique ce soit : C’est mesquin ! La faute à qui ? Les objets ? Les références ? Pourquoi ? Comment ? Quel suspens. Je me vois tout à coup dans une série policière, mon enquête peut débuter. Mon premier témoin sera la doc officielle : Elle a toujours quelque chose d’intéressant à raconter.
2. Les objets démystifiés! On m’aurait menti ?
Après une rapide recherche, j’arrive sur une page de la doc officielle qui parle des références (http://www.php.net/manual/fr/language.oop5.references.php). On peut y lire ceci :
« Depuis PHP 5, un une variable objet ne contient plus l’objet en lui-même. Elle contient seulement identifiant d’objet, qui permet aux accesseurs d’objets de trouver cet objet. Lorsque l’objet est utilisé comme argument, retourné par une fonction, ou assigné à une autre variable, les différentes variables ne sont pas des alias (ndlr : références) : elles contiennent des copies de l’identifiant, qui pointent sur le même objet. »
Première information importante, on ne manipule pas directement un objet, mais un identifiant. Pour info, c’est de l’hexadécimal (ex : 0000000041ae805200000000642707c6). Pour le connaitre, vous pouvez utiliser la méthode « spl_object_hash ».
Deuxième chose intéressante, lorsque l’on passe un objet en paramètre d’une fonction (ou qu’on l’assigne à une autre variable), on ne lui donne, en fait, qu’une copie de son identifiant. Une copie ? Cela ressemble furieusement à ce qu’il se passe pour un tableau, un entier ou une chaine de caractères, non ? Hé bien oui, c’est pareil !
Pour en avoir le cœur net, lançons un petit test :
1 2 3 4 5 6 7 8 9 |
function add($o){ $o->i++; $o = null; } $monObjet = new stdClass(); $monObjet->i = 1; add($monObjet); echo $monObjet->i; //Affiche 2 |
Il n’y a rien d’incroyable dans cet exemple, si ce n’est que cela confirme les dires de la doc :
- « $o » a le même identifiant que « $monObjet ». Ce qui nous permet de modifier le contenu et d’appeler les méthodes du même objet.
- La fonction « add » reçoit une copie de l’identifiant. En effet, la variable « $o », interne à la fonction, a beau être écrasée par « null », cela n’impacte pas l’objet extérieur. Pourquoi ? Tout simplement parce que ce n’est pas une référence. L’effet est donc le même que pour un paramètre de type entier, tableau ou autre.
Le mythe est tombé : En objet, on travaille donc aussi avec des « copies ». Certes, ce sont des copies d’identifiant et pas des copies de valeur (« clone ») comme pour tous les autres types, mais c’est la seule subtilité. Ce stratagème « made in PHP5 » nous permet de modifier le contenu d’un même objet, un peu partout dans le code, sans savoir si celui-ci a bien été donné/assigné en référence. Les objets n’ont donc plus rien à voir avec les références.
Il était important de connaitre ce principe pour attaquer la deuxième partie de l’enquête : Les références/alias.
3. Les références/alias… Aïe, j’ai mal
Pour commencer, revenons sur la doc officielle (http://www.php.net/manual/fr/language.oop5.references.php) qui nous donne un premier indice sur les références :
« Une référence PHP est un alias, qui permet à deux variables différentes de représenter la même valeur »
On parle bien de variables. J’en conclu que, dans le monde des références, tout le monde est logé à la même enseigne, y compris les objets. On aura beau travailler avec une chaine de caractère, un entier ou un objet, le résultat sera le même. Tant mieux, s’il n’y a pas d’exception, ce sera plus facile à expliquer J Cependant, laissez-moi vous dire que ce ne sera pas, pour autant, une partie de plaisir.
Petit rappel important avant de commencer: Lorsqu’une méthode reçoit un paramètre en référence (&$monParam) pour l’assigner à une variable interne d’un objet, assurez-vous de bien assigner la variable par référence, sinon PHP vous en fera une copie. Un simple exemple :
1 2 3 4 5 6 7 8 9 10 11 |
class Operation{ public $a = 0; public function incremente(&$a){ //Incrémente $this->a = $a; //Pas de « & » à l’assignation $this->a ++ ; //J’incrémente en pensant aussi incrémenter « $a » reçu en paramètre } } $aGlobal = 1 ; $op = new Operation() ; $op->incremente($aGlobal) ; //Je passe mon $aGlobal, par référence, donc je m’attends à ce que « add » modifie ma variable. echo $aGlobal ; //Affiche 1 |
Comme vous pouvez le voir, indiquer au paramètre que c’est une référence n’est pas toujours suffisant. Faites donc bien attention lorsque vous travaillez avec des références dans des objets. Un simple oubli et je vous garantis de longues journées « à la recherche du & perdu ».
Ceci étant dit, revenons-en à nos moutons ! Pour bien comprendre les références, commençons simplement par écrire une méthode qui recevra 2 paramètres :
- Le premier est passé en référence car on veut que la variable donné en paramètre (l’originale, c’est-à-dire « $un ») soit aussi modifié.
- Le deuxième est une simple valeur passée en copie.
1 2 3 4 5 6 7 |
function copy_param_ref(&$a, $b){ $a = $b; //Assignation par copie } $un = 1 ; $deux = 2 ; copy_param_ref ($un, $deux) ; echo $un ; //Affiche 2 |
A présent, faisons le même test, mais avec une assignation par référence (=&). Cet exemple n’est pas pris au hasard, puisque je suis souvent tombé dessus lors de la migration de notre application de PHP4 à PHP5.3.
1 2 3 4 5 6 7 |
function copy_full_ref(&$a, $b){ $a = &$b; //Assignation par référence } $un = 1 ; $deux = 2 ; copy_full_ref($un, $deux) ; echo $un ; //Affiche 1 |
Aïe, alors que nous avons utilisé les références, notre variable « $un », n’a pas été modifiée ! C’est comme si aucune référence n’avait existé. Trop de références, tuerait-elle la référence ? Afin d’en avoir le cœur net, j’ai décidé d’écrire tous les cas possibles pour, peut-être, avoir des indices sur leurs manières de fonctionner. Voici la liste des 8 cas :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//Les fonctions qui reçoivent « $a » et « $b » par référence. L’assignation se fait alternativement par référence ou par copie function paramAB-assign_ref (&$a, &$b){$a = &$b;} //Assignation par référence function paramAB-assign_copy (&$a, &$b){$a = $b;} //Assignation par copie //Les fonctions dont « $a » est une référence. L’assignation se fait alternativement par référence ou par copie function paramA-assign_ref (&$a, $b){$a = &$b;} //assignation par référence function paramA-assign_copy (&$a, $b){$a = $b;} //Assignation par copie //Les fonctions dont « $b » est une référence. L’assignation se fait alternativement par référence ou par copie function paramB-assign_ref ($a, &$b){$a = &$b;} //assignation par référence function paramB-assign_copy ($a, &$b){$a = $b;} //Assignation par copie //Les fonctions qui ont « $a » et « $b » en copie. L’assignation se fait alternativement par référence ou par copie function param-assign_ref ($a, $b){$a = &$b;} //assignation par référence function param-assign_copy ($a, $b){$a = $b;} //Assignation par copie |
Je me suis amusé à tester chacune d’entre elles de cette manière (exemple avec la première méthode de la liste) :
1 2 3 4 |
$un = 1 ; $deux = 2 ; paramAB-assign_ref ($un, $deux) ; //Appel à une des 8 fonctions echo $un ; |
Le résultat final de la variable « $un » sera notre indicateur : Vaut-elle 1 ou 2 ? Une fois les exécutions terminées, j’ai pu regrouper les méthodes en deux ensembles :
- « $un » vaut « 1 »:
- paramAB-assign_ref
- paramA-assign_ref
- paramB-assign_ref
- param-assign_ref
- paramB-assign_copy ¹
- param-assign_copy ¹
- « $un » vaut « 2 »:
- paramAB-assign_copy
- paramA-assign_copy
¹: Attention toutefois, en Php 5.3, on peut encore (même si c’est déprécié) forcer un paramètre qui demande une copie (pas de & devant le nom de la variable du paramètre) à recevoir une référence, en l’indiquant explicitement lors de l’appel à la fonction (Ex : maFonction (&$a,&$b)). Si vous faites cela, alors « paramB-assign_copy » et « param-assign_copy » changeront aussi la valeur de « $un » et seront donc dans le deuxième ensemble.
Voici ce que l’’on peut en conclure :
- Il est visiblement acquis que donner « $deux » en référence ou en copie, ne change strictement rien au résultat de « $un ». Ça se tient ! Au final que « $b » soit une référence ou non, c’est la valeur de « $un » qui nous intéresse.
- Assigner par référence ne change pas « $un » non plus… Ça c’est étrange, mais nous y reviendrons !
Par contre, pour modifier la variable « $un », il faut deux conditions:
- « $un » doit être passé en référence. En effet, passer la variable en copie, ne changera que la valeur de…sa copie.
- L’assignation doit se faire via un copy (« = ») et non pas par référence (« = & »).
Tout est clair, sauf un cas: Pourquoi « $un » n’est pas modifié pour les fonctions qui assignent par référence (« paramA-assign_ref » et « paramAB-assign_ref ») ? Voici un exemple plus simple pour expliquer le problème :
1 2 3 4 |
$a = 1 ; $b = 2 ; $a1 = &$a ; //$a1 est une référence vers « $a » $a1 = $b ; //$a change également de valeur et vaut 2 |
« $a » vaut 2 car « $a1 » est une référence vers « $a ». Si par contre, j’assigne par référence…
1 2 3 4 |
$a = 1 ; $b = 2 ; $a1 = &$a ; //$a1 est une référence vers « $a » $a1 = &$b ; //$a ne change pas de valeur et vaut donc toujours 1 |
« $a » vaut toujours 1 et n’a pas changé ! Mais pourquoi ? La réponse n’est pas évidente et n’est indiquée nulle part dans la documentation.
Heureusement la communauté veille, et visiblement, je ne suis pas le premier à m’être posé la question. Après avoir épluché les commentaires des développeurs, tous en sont arrivés à la même conclusion : Lorsque vous assignez par référence une variable à une autre, seule la variable concernée est modifiée (dans notre cas, la variable « $a1 »). Il n’y a donc pas de dommage collatéral pour les variables ayant la même référence, elles continuent de pointer vers la même valeur et ne sont pas impactées par l’opération. Fou, n’est-ce pas ?
Je vous laisse avec un dernier casse-tête.
1 2 3 4 5 |
$a = 1; $b = 2; $a1 = &$a; //$a1 est une référence vers $a $a2 = &$a; //$a2 est aussi une référence vers $a $a = &$b ; //Maintenant, $a est une référence vers $b |
A votre avis, que vaut « $a1 » et « $a2 » ? On pourrait se dire : « $a » est une référence vers « $b », et comme « $a1 » et « $a2 » sont des références vers « $a », alors « $a1 » et « $a2 » sont également modifiés… Bien essayé, mais ça n’est pas le cas J. En effet, « $b » est assigné à « $a » par référence, donc seul « $a » change. CQFD.
4. Conclusion
Au terme de cet article, que pouvons-nous retenir ?
Tout d’abord, ne dites plus qu’un objet est une référence : C’est un raccourci rapide pour dire que vous ne devez plus utiliser les références lorsque vous passez votre objet à une méthode. Un objet « est » un identifiant (voir «spl_object_hash ») qui est copié lorsqu’il est donné en paramètre ou assigné à une autre valeur, comme le sont les chaines de caractères, les tableaux ou les entiers.
Ensuite, on vous l’a sans doute déjà dit mais, n’utilisez jamais les références à moins de savoir vraiment ce que vous faites (et de l’indiquer pour ceux qui reprendront votre code)! Comme vous avez pu le constater, c’est un nid à problèmes. Selon moi, utiliser des références est souvent preuve d’une mauvaise architecture. Et contrairement à ce que l’on pense, les références n’améliorent pas les performances, pire, elles pourraient les dégrader dans pas mal de cas…
mhhh…décidément, elles sont vicieux ces références. Je suis tout à fait d’accord avec la conclusion: « utiliser des références est souvent preuve d’une mauvaise architecture ».
« Deuxième chose intéressante, lorsque l’on passe un objet en paramètre d’une fonction (ou qu’on l’assigne à une autre variable), on ne lui donne, en fait, qu’une copie de son identifiant. Une copie ? Cela ressemble furieusement à ce qu’il se passe pour un tableau, un entier ou une chaine de caractères, non ? Hé bien oui, c’est pareil ! »
à strictement parler, oui c’est la même action de copie, mais les conséquences sont différentes:
si on prend ce code, on le voit bien:
class C {
private $v;
public function setV($v) {
$this->v = $v;
}
public function getV() {
return $this->v;
}
}
function setV($o, $v) {
$o->setV($v);
}
$obj = new C();
$obj->setV(1);
echo $obj->getV(). »\n »;
setV($obj, 2);
echo $obj->getV(). »\n »;
function addV($a, $v) {
$a[] = $v;
}
$arr = array(1);
addV($arr, 2);
var_dump($arr);
l’attribut $v de l’objet de classe C prend bien les valeurs 1 et 2, même si la valeur 2 lui a été donnée dans un appel de fonction.
alors que le tableau ne garde pas la valeur 2 qui lui a été ajoutée dans un appel de fonction.
en fait les objets passés en paramètre ne sont pas des références, mais des pointeurs (il y a la même confusion en Java en général).
Tout à fait. Maintenant, je ne sais pas si le terme « pointeur » est réservé au language C (il me tentait de l’écrire), mais, c’est dans l’idée 🙂
Anthony Ferrara sur les références en PHP:
https://www.youtube.com/watch?v=_YZIBWQr_yk&index=8&list=PLM-218uGSX3DQ3KsB5NJnuOqPqc5CW2kW