Soutenez-nous

Maitrise de la gestion des variables en PHP

Image non disponible


précédentsommairesuivant

VII. Astuces et pièges concernant les références

Le plus bel exemple que l'on donne souvent est celui-ci :

Les pièges pas faciles des références en PHP
Sélectionnez
<?php
$a = array(1, 2, 3);
foreach ($a as &$v) { }
foreach ($a as $v) { }

// $a vaut array(1, 2, 2) ici

Que s'est-il passé dans l'exemple ci-dessus ?
C'est en fait assez simple : lorsque le premier foreach utilisant une référence sur les valeurs du tableau termine, en mémoire, $v est une référence sur la dernière case du tableau $a (la boucle foreach ayant terminé l'itération).
Autrement dit: entre les deux foreach, $v est liée à la dernière case du tableau (son index 2) et toute modification future de la variable $v changera la valeur de la dernière case du tableau. Eh bien, cette modification a lieu ! C'est le deuxième foreach qui itère sur le tableau depuis le début et remplit à chaque itération $v (donc aussi la dernière case du tableau) respectivement des valeurs 1, 2... puis 2.

Comment corriger ce code pour qu'il fonctionne correctement ? Ne pas utiliser de référence, oui certes, mais aussi utiliser unset($v) entre les deux foreach, ou encore utiliser autre chose que la variable $v dans le deuxième foreach.
Comme exercice, vous pourriez tracer les schémas mémoire des zvals et leurs valeurs de is_ref et refcount sur cet exemple.

Il est aussi remarquable que le Zend Engine n'utilise pas les mêmes règles pour accéder à des variables par référence que par copie. La différence la plus notable vient du fait que si la variable n'existe pas, lorsqu'on tente d'en prendre une référence, le Zend Engine va alors créer la variable, lui assigner NULL puis la lire, alors que par copie, il va tenter de la lire et va renvoyer une erreur si celle-ci n'existe pas.

Attention aux variables inexistantes prises par référence
Sélectionnez
<?php
/* Le moteur tente de lire $b pour l'affecter par copie à $a.
   Ici, $b n'existe pas, une erreur est levée */
$a = $b; /* Notice: Undefined variable $b */


/* Ici, le comportement du moteur est tout autre : il va tenter d'accéder
   en écriture à $d, à cause de la référence. Or si $d n'existe pas, il crée
   alors la variable et lui affecte NULL, puis il tente de lire $d, qui cette fois-ci existe */
$c = &$d; /* Aucune erreur, $c et $d sont des références et valent NULL */

Comportement auquel on est plus habitué lorsqu'on utilise des appels de fonctions avec des références :

Exemple de création de variable au moyen d'une référence dans un appel de fonction
Sélectionnez
<?php
/* preg_match() prend son 3ème argument par référence, un cas très connu */
$var = "foobar";
preg_match('/.*/', $var, $matches);

var_dump($matches); /* Fonctionne, preg_match() a crée et rempli $matches */

Concernant les fonctions et les références :

Une fonction créatrice de référence
Sélectionnez
<?php
function &make_ref(&$var)
{
    return $var;
}

$a = 8;
$b = &make_ref($a); // $a et $b sont des références vers la même zval

Le code ci-dessus dessine une fonction qui crée une référence d'une variable sur une autre. Le piège ici est de bien retourner par référence ET affecter ce retour par référence.
Voyons un autre exemple :

 
Sélectionnez
<?php
function &foo(&$param) {
    $param = 99;
    return $param;
}

$a = 1;
// $a vaut 1
$b = &foo($a);
// $a vaut 99, $b vaut 99
$b = 2;
// $b vaut 2, $a vaut 2
Même exemple que ci-dessus mais en oubliant l'affectation par référence
Sélectionnez
<?php
function &foo(&$param) {
    $param = 99;
    return $param;
}

$a = 1;
// $a vaut 1
$b = foo($a); // il manque l'affectation par référence &
// $a vaut 99, $b vaut 99
$b = 2;
// $b vaut 2, $a vaut toujours 99

Ce piège est classique, la fonction retourne bien une référence (il y a un '&' devant son nom à la déclaration), mais l'affectation de son retour se fait par valeur et non par référence. La référence est donc perdue.
Inversement, l'affectation par référence du retour d'une fonction sans référence fonctionne, mais provoque une erreur de type E_STRICT :

Code incorrect: la fonction ne retourne pas de référence mais on affecte son retour par référence
Sélectionnez
<?php
function foo()
{
    $a = 8;
    return $a;
}

$b = &foo() /* PHP Strict Standards:  Only variables should be assigned by reference */

Attention aussi, les références se font sur des variables :

Seule une variable peut être passée par référence
Sélectionnez
<?php
function take_by_ref(&$ref)
{
    // du code ici
}

take_by_ref("some string"); // Fatal Error: Only variables can be passed by reference
Seule une variable peut être affectée par référence
Sélectionnez
<?php
function &foo()
{
    return 'string';
}

$b = &foo(); /* PHP Notice:  Only variable references should be returned by reference */

/* Ce n'est qu'un Notice, le script continue, le moteur s'est comporté comme si l'affectation était
   faite par copie, il a copié la chaine 'string' dans un nouveau zval affecté à $b */

On pourrait aussi parler des chaines de caractères auxquelles on ne peut accéder par référence à des caractères :

Accéder à un caractère dans une chaine par référence mène à une erreur fatale
Sélectionnez
<?php
$s = 'string';
$c = &$s[1]; /* Fatal error: Cannot create references to/from string offsets nor overloaded objects */

Dernière recommandation : n'utilisez des références qu'en cas de besoin. Par exemple, la majorité des fonctions PHP prend en paramètre des copies, ceci signifie que si vous avez utilisé précédemment une référence, vous allez forcer PHP à en créer une copie :

Utilisation d'une référence qui force PHP à dupliquer la zval mémoire
Sélectionnez
<?php
function ma_fonction(&$var)
{
    if (strlen($var) == 20) {
        $var .= "foo";
    }
}

$a = "une chaine de caractères";
ma_fonction($a);

Dans le code ci-dessus, l'appel à la fonction strlen() va obliger PHP à dupliquer la zval de $var sur la pile de strlen(), car il ne peut utiliser la même zval puisqu'elle a été passée par référence. Ici, la référence produit un effet inverse : elle va obliger PHP à dupliquer la zval de $var lors de l'appel à strlen(), alors que si $var avait été passée par valeur à ma_fonction(), strlen() aurait simplement utilisé la même zval et ne l'aurait dupliqué que si elle avait effectué une opération d'écriture dessus (le CopyOnWrite) or ce n'est pas le cas de la fonction strlen().


précédentsommairesuivant

  

Copyright © 2010 Julien Pauli. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.