Créer une instance
à la manière de PDO
et du mode FETCH_CLASS

C'est facile de créer une instance de classe, un petit new et on en obtient une toute neuve. Mais comment créer une instance à la manière de PDO et du mode de récupération FETCH_CLASS qui renseigne les propriétés de l'instance avant que le constructeur ne soit appelé ? PHP ne propose aucune fonction pour cela, pourtant il en existe une qui permet d'obtenir des instances dont les propriétés sont renseignées. Ce pourrait-il que l'on puisse tromper cette fonction pour en faire autant ?

Enquête sur la dé-sérialisation

Si l'on sérialise un objet avec la méthode serialize() on obtient une chaîne de caractères que l'on peut transformer à nouveau en instance avec la fonction unserialize(), c'est comme de la magie à double sens. Voyons ce que donne cette magie sur ce superbe tableau associatif :

<?php

$properties = array
(
    'a' => 'Ceci ',
    'b' => 'est un ',
    'c' => 'test.'
);
a:3:{s:1:"a";s:5:"Ceci ";s:1:"b";s:7:"est un ";s:1:"c";s:5:"test.";}

Et maintenant si l'on caste ce tableau en objet avant de le passer à serialize() :

O:8:"stdClass":3:{s:1:"a";s:5:"Ceci ";s:1:"b";s:7:"est un ";s:1:"c";s:5:"test.";}

Comme on pouvait s'y attendre, il n'y a pas grand chose qui différencie les deux sérialisations, les propriétés sont sérialisées de la même façon, seul le début de la chaîne qui indique le type de donnée change. Ce pourrait-il qu'en trafiquant ce début de chaîne (O:8:"stdClass") on puisse obtenir une instance d'une autre classe avec les mêmes propriétés ?

Trafiquer une chaîne sérialisée

On va donc trafiquer une chaîne sérialisée pour qu'à la dé-sérialisation on obtienne une instance de notre classe de test. Imaginons une classe des plus simples :

<?php

class Test
{
    public $a;
    protected $b;
    private $c;
    public $d;

    public function __construct()
    {
         $this->d = $this->a . $this->b . $this->c;
    }
}

Il y a quatre propriétés dont deux sont publiques, une protégée et une privée. Le constructeur concatène les valeurs des propriétés a, b et c et place le résultat dans la propriété d.

Si tout va bien, notre fonction justement nommée instantiate() nous permettra d'obtenir une instance de Test initialisée avec les propriétés de nos rêves.

<?php

function instantiate($class_name, array $properties=array(), array $construct_args=array())
{
    $serialized = serialize((object) $properties);
    $serialized = 'O:' . strlen($class_name) . ':"' . $class_name . '"' . substr($serializedstrlen('O:8:"stdClass"'));

    $instance = unserialize($serialized);

    if (method_exists($instance'__construct'))
    {
        call_user_func_array(array($instance'__construct')$construct_args);
    }

    return $instance;
}

Et maintenant, le moment de vérité :

<?php

var_dump(instantiate('Test'$properties));
object(Test)[11]
  public 'a' => string 'Ceci ' (length=5)
  protected 'b' => null
  private 'c' => null
  public 'd' => string 'Ceci ' (length=5)
  public 'b' => string 'est un ' (length=7)
  public 'c' => string 'test.' (length=5)

Patatra ! C'est pas vraiment ce à quoi l'on rêvait. La propriété a a la bonne valeur, mais les propriétés b et c sont en double avec une portée publique, et finalement d ne contient que la valeur de a. Manifestement les portées protégées et privées posent problème…

Un problème de portée

Malheureusement, les propriétés protégées et privées ne peuvent être définies avec une simple modification du type de donnée de la chaîne sérialisée d'un tableau associatif. Mais à quoi ressemble la chaîne sérialisée d'une instance de Test au juste ?

string 'O:4:"Test":4:{s:1:"a";N;s:4:"�*�b";N;s:7:"�Test�c";N;s:1:"d";s:0:"";}' (length=69)

Effectivement, cela ne ressemble par vraiment à la chaîne sérialisée de notre tableau associatif, le nom des propriétés protégées et privées sont précédées par de drôles de caractères : les propriétés protégées sont préfixés par "\x00*\xx" et les propriétés privées sont préfixées par "\x00<class_name>\x00"<class_name> est le nom de la classe définissant la propriété. La méthode précédente ne convient donc pas puisque les noms sérialiés ne correspondent pas à ce qu'ils devraient être, des noms publics sont utilisés, ce qui fait que les valeurs ne peuvent être définies à cause de la portée des propriétés.

Pas le choix, il nous faut créer nous même la chaîne sérialisée afin de prendre en compte la portée des propriétés de la classe.

Prendre en compte la portée des propriétés

Il nous faut prendre en compte la portée des propriétés pour les sérialiser avec un nom adéquat. Pour cela nous allons utiliser une réflexion de la classe à instancier et vérifier la portée des propriétés pour adapter leur nom en conséquence. On prendra garde à ne pas sérialiser les propriétés statiques afin de ne pas causer d'explosion et on n'oubliera pas d'ajouter des propriétés publiques pour celles qui ne sont pas définies par la classe.

<?php

function instantiate($class_name, array $properties=array(), array $construct_args=array())
{
    $class_reflection = new \ReflectionClass($class_name);
    $properties_count = 0;
    $serialized = '';

    if ($properties)
    {
        $class_reflection = new \ReflectionClass($class_name);
        $class_properties = $class_reflection->getProperties();
        $defaults = $class_reflection->getDefaultProperties();

        $done = array();

        foreach ($class_properties as $property)
        {
            if ($property->isStatic())
            {
                continue;
            }

            $properties_count++;

            $identifier = $property->name;
            $done[] = $identifier;
            $value = null;

            if (array_key_exists($identifier$properties))
            {
                $value = $properties[$identifier];
            }
            else if (isset($defaults[$identifier]))
            {
                $value = $defaults[$identifier];
            }

            if ($property->isProtected())
            {
                $identifier = "\x00*\x00" . $identifier;
            }
            else if ($property->isPrivate())
            {
                $identifier = "\x00" . $property->class . "\x00" . $identifier;
            }

            $serialized .= serialize($identifier) . serialize($value);
        }

        $extra = array_diff(array_keys($properties)$done);

        foreach ($extra as $name)
        {
            $properties_count++;

            $serialized .= serialize($name) . serialize($properties[$name]);
        }
    }

    $serialized = 'O:' . strlen($class_name) . ':"' . $class_name . '":' . $properties_count . ':{' . $serialized . '}';

    $instance = unserialize($serialized);

    if (method_exists($instance'__construct'))
    {
        call_user_func_array(array($instance'__construct')$construct_args);
    }

    return $instance;
}

https://gist.github.com/1166786#file_instantiate.php

On croise les doigts et on regarde ce que ça donne :

object(Test)[16]
  public 'a' => string 'Ceci ' (length=5)
  protected 'b' => string 'est un ' (length=7)
  private 'c' => string 'test.' (length=5)
  public 'd' => string 'Ceci est un test.' (length=17)

C'est parfait ! Toutes les propriétés ont les bonnes valeurs et d contient bien « Ceci est un test. ».

Conclusion

Nous sommes maintenant capables de créer des instances à la manière de PDO et du mode FETCH_CLASS. Cela dit, parce que nous utilisons la fonction unserialize(), si la méthode __wakeup() est définie par la classe de l'instance, elle sera appelée, et elle sera appelée avant le constructeur… à prendre en compte donc.

La fonction peut être transformée en méthode statique de classe, dans ce cas on utilisera la fonction get_called_class() pour obtenir le nom de la classe dont la méthode a été appelée, comme le fait la classe ICanBoogie\Object.

Laisser un commentaire

Pas de commentaire