26/03/2013

Mais... on peut faire "ça" en PHP ? Mais c'est horrible !

Pour changer, je ne vais pas parler de ce qui est super avec PHP, mais plutôt de ce qui pue dans PHP. Et oui...

Attention, qu'on ne me fasse pas dire ce que je n'ai pas dit : j'adore PHP ! C'est la techno que j'utilise tous les jours, c'est un beau langage, riche et puissant, mais aussi très souple, et il est facile de faire n'importe quoi avec. Les codes suivants sont des exemples de ce qu'on peut faire grâce (ou à cause) de cette souplesse.

Ces techniques sont vraiment crades, mais peuvent parfois, en dernier recours, être utiles. Après, si vous voyez un code qui les utilise, posez-vous la question : est-ce PHP qui est crade, ou bien n'est-ce pas plutôt le développeur derrière qui ne s'est pas posé assez de questions ?

Redéfinir la portée d'un attribut

Commençons doucement. Un attribut privé est un attribut qui n'est accessible que par la classe qui le possède (par définition)... Ah bon ? Et si on essaye de changer sa portée ?

class Foo
{

    private $bar = 5;

    public function bar()
    {
        return $this->bar;
    }

}

$foo = new Foo;


$attribute = new ReflectionProperty($foo, 'bar');
$attribute->setAccessible(true);
$attribute->setValue($foo, 'nouvelle valeur');

var_dump($foo->bar());
// string(15) "nouvelle valeur"

Nom de zeus ! On vient de modifier la valeur d'un attribut privé !

Lire des attributs privés

On a pu changer la portée d'une variable, mais l'honneur est sauf : il n'est pas possible de lire un attribut privé, tant qu'il reste privé.

Bon, il doit bien y avoir une solution, non ? Essayons avec un petit coup de serialize().

class Foo {
    private $bar = 5;
}

$foo = new Foo;
$serialized = serialize($foo);

preg_match('!bar";(.):(.*);!', $serialized, $matches);
list(,$type,$value) = $matches;

var_dump("bar vaut $value");
// bar vaut 5
Facile ! Bien sûr, l'exemple est trivial, il existe des solutions plus complètes.

Hériter... directement de ses grand-parents

L'héritage c'est pratique ; un objet hérite de son parent, qui lui-même hérite d'un autre objet, le tout dans une harmonie parfaite.

Mais attendez, saviez-vous qu'il est possible d'hériter de sa classe grand-mère, sans passer par sa classe Mère ? (par pitié, n'essayez pas de vous représenter ce que ça ferait dans la vraie vie)

class GrandMother {
    public $bar;
    public function foo() {
        return $this->bar. ' in GrandMother';
    }
}

class Mother extends GrandMother {
    public function foo() {
        return $this->bar. ' in Mother';
    }
}

class Child extends Mother {
    public function foo() {
        return GrandMother::foo();
    }
}

$child = new Child;
$child->bar = 'abc';
var_dump($child->foo());
// string(18) "abc in GrandMother"

Il suffit donc de ne pas utiliser le mot-clef "parent", mais directement le nom de la classe Grand-mère. Le contexte ($this) est bel et bien préservé. C'est un reliquat de PHP 4...

Ne pas donner de nom à une variable

Toute variable possède un nom : $i, $foo, $bar... Que se passe t-il si vous tenez de déclarer une variable sans lui donner de nom ?

$ = 5;
// PHP Parse error:  syntax error, unexpected '=', expecting variable

Arf... Mais attendez, ne peut-on pas utiliser l'évaluation dynamique des noms de variables pour créer une variable dont le nom serait une chaîne vide ? Essayons :

$foo = '';
${$foo} = 5;

print_r(get_defined_vars());
//    ...
//    [foo] =>
//    [] => 5
//)

Et bien si, on peut. Constatez au passage que la variable est bien visible dans le get_defined_vars().

Du coup, on peut s'amuser à faire plein de trucs pas très utiles, comme nommer sa varaible $0 par exemple :

echo $0;
// syntax error, unexpected '0' ...

echo ${0};
// ok

Transtyper un objet

Un objet est d'une classe donnée. Chaque classe possède un contexte propre (des attributs) et son propre comportement.

Du coup il ne devrait pas être possible de réutiliser un objet A pour en faire un objet B. Et pourtant ...

class Foo
{

    public $a = 5;
    public $b = 6;

}

class Bar
{

    public $a = 'abc';
    public $b = 'def';

}

$foo = new Foo;
$bar = new Bar;


$object = convert($foo, 'Bar');
print_r($object);
//Bar Object
//(
//    [a] => 5
//    [b] => 6
//)

Comment a t-on fait ? C'est assez simple, il suffit une fois de plus de sérializer notre objet, de modifier une valeur dans la chaîne obtenue, puis de créer un nouvel objet.

function convert($object, $class)
{
    $serialized = serialize($object);

    $className = get_class($object);
    $len = strlen($className);
    $start = $len + strlen($len) + 6;


    $serializedInfos = 'O:' . strlen($class) . ':"' . $class . '":';
    $serializedInfos .= substr($serialized, $start);

    return unserialize($serializedInfos);
}

Vous trouverez de nombreux exemples sur le net.

"Ecouter" les changements d'une variable

Vous connaissez sûrement la fonction JavaScript watch(), très pratique, qui permet d'écouter les changements qui peuvent survenir sur une variable.

Vous me voyez venir : il est possible (à grands frais : lenteurs, charge mémoire, etc !) de faire la même chose en PHP. Je vous aurai prévenu : ne faites pas ça en prod !

L'idée consiste à utiliser une fonction qui va être exécutée à chaque tick, pour observer si la variable que l'on souhaite "écouter" à été modifiée ou non. Si c'est le cas, un simple appel à debug_baktrace() nous permettra de savoir comment cette variable a été modifiée.

$var = 'abc';

function tick()
{
    global $var, $expectedVar;
    if (isset($var)) {
        if (isset($expectedVar) && $var !== $expectedVar) {

            //
            // La variable a été modifiée
            $context = debug_backtrace();
            $where = (isset($context[1]['class']) ? $context[1]['class'] . '::' : '')
                    . $context[1]['function'] . '()';

            printf('la variable $var a été modifiée par %s (fichier %s, ligne %d), et vaut désormais "%s"'
                    , $where, $context[1]['file'], $context[1]['line'], $var);
        }
        $expectedVar = $var;
    }
}

//
// Enregistrons notre pseudo "écouteur"
register_tick_function("tick");
declare(ticks = 1);

Vérifions :

//
// L'heure du test
function foo()
{
    global $var;
    $var = 'def';
}

foo();
//
// Affiche :
//
// la variable $var a été modifiée par foo()
// (fichier /home/data/www/jeff/misc/php-berk/watch-var.php, ligne 39),
// et vaut désormais "def"

C'est tout ?

J'hésite à aller plus loin : on pourrait parler encore de ce qu'il est possible de faire avec les __PHP_Incomplete_Class, de la confusion possible à utiliser des fonctions comme class_alias() (quoique parfois utile), de ce qu'il est affreusement possible de faire avec runkit, dela possibilité de remplacer $_GET par une valeur de notre choix... Mais je crois que ça suffira là :) .

Bref, vous l'aurez compris, si je suis vraiment convaincu que PHP est un super langage, je vois passer beaucoup de mauvais code, de mauvais développeurs et de mauvaises pratiques, qui me font quotidiennement prendre conscience qu'il est possible de faire vraiment n'importe quoi en développement. Lorsque l'on sort des sentiers battus, on doit avoir une bonne raison et comprendre pourquoi on le fait.

PHP est un beau langage, ce n'est pas parce qu'il est "facile" de débuter en PHP qu'il faut en faire n'importe quoi. Heureusement, on peut aussi faire de très belles choses avec ! Et c'est justement ça le job d'un développeur.

blog comments powered by Disqus