À certains moments, on apprécierait de pouvoir enregistrer un array en intégralité (c'est-à-dire avec les clefs associées aux valeurs) en base de données, ou encore dans un fichier. Des possibilités s'offrent alors à nous : on pourrait, par exemple dans le cas d'un fichier, parcourir l'array via un foreach, afin d'écrire ligne par ligne chaque élément… Mais cet exemple devient impossible dans le cas d'un array multidimensionnel, par exemple.
On pourrait alors utiliser la récurrence… Et on se rend alors compte que cela fait beaucoup de bruit pour rien. En effet, il existe une fonction bien particulière et ô combien puissante dans ce genre de cas en PHP : serialize().
Le principe de serialize() est simple, et bien résumé dans la documentation PHP :
Citation : Documentation PHP
serialize() retourne une chaîne contenant une représentation linéaire de value, pour stockage. C'est une technique pratique pour stocker ou passer des valeurs de PHP entre scripts, sans perdre ni leur structure, ni leur type.
Ah, c'est simple et bien résumé, cela ?
Eh bien mine de rien, oui. Cela signifie simplement qu'on peut « transtyper » une variable, quel que soit son type, en une chaîne de caractères.
Pour comprendre simplement, testons ce code :
<?php
$notes = array(7,3,8,9); // Formation d'un array pour la forme
echo serialize($notes); // echo du résultat de serialize() sur cet array
?>
Citation : Affichage
a:4:{i:0;i:7;i:1;i:3;i:2;i:8;i:3;i:9;}
Un echo suffit, car serialize() retourne une chaîne de caractères (en tout cas, s'il n'y a aucune erreur). Cela nous affichera… quelque chose de peu compréhensible, certes. Mais on arrive à voir que nos valeurs sont toujours là et dans le même ordre, même si c'est sous une forme différente.
serialize() prend la variable, et cherche d'abord son type. Cette définition de type sera conservée (i pour integer, b pour booléen, d pour double, s pour string et a pour array). Selon ce type, il va chercher le nombre d'éléments ou non. Ce nombre d'éléments peut être la longueur d'une chaîne de caractères ou le nombre d'éléments d'un tableau. Ensuite, serialize() définit le contenu selon ce type. Dans le cas d'un entier, le contenu ne change pas. Serializer un entier reviendrait donc à grossir juste la taille (en mémoire) prise par la variable… Oublions donc les entiers. Pour un booléen, serialize() traduit la valeur en entier (TRUE devient 1 et FALSE devient 0) par transtypage classique. On revient à la même problématique qu'avec les entiers : on évitera de serializer un booléen. De même pour une chaîne, à noter que serialize() place des guillemets autour des chaînes, ce qui peut être intéressant à savoir dans le cas d'un enregistrement en base de données. Vous remarquerez sûrement que notre nombre décimal est devenu… quelque peu bizarre. Cela est dû à une mauvaise précision de calcul de serialize() qui n'est réellement pas fait pour traiter des données aussi simples.
Passons à nos chers arrays ! Il semblerait que serialize() répète la procédure précédente (déterminer le type, puis le nombre d'éléments, puis la valeur…) pour chaque élément. Une dimension d'un array est délimitée par des accolades. Et les clefs n'en sont pas exemptes et semblent traitées… comme des valeurs simples !
Mais alors, comment différencier les clefs des valeurs ?
C'est là que joue le nombre d'éléments. :) Si l'on a un seul élément, mais qu'on a deux valeurs dans l'array serializé, c'est qu'il s'agit d'une paire clef/valeur. De toute façon, tout array est normalement indexé numériquement en interne, par PHP. D'où notre premier affichage.
Et comme on a ainsi une chaîne de caractères (conservant néanmoins parfaitement notre array), on peut facilement la manipuler, pour la transmettre de page en page, l'envoyer en base de données, ou encore l'écrire dans un fichier.
Ah ouais, super… Mais on ne peut rien en faire de cette chaîne de caractères !
Et pof ! Voici unserialize().
Cette superbe fonction peut se résumer en une phrase : elle fait l'inverse de serialize(). En partant d'une chaîne de caractères provenant de serialize(), unserialize() récupère les données et renvoie… les données originales. Concrètement, cela signifie qu'en testant ceci :
<?php
$notes = array('maths'=>1,'svt'=>8,'algo'=>6,'philo'=>5); // Un array…
$serialized = serialize($notes); // On serialize et on stocke cette chaîne.
echo '<pre>'; // Les balises « pre » permettent d'afficher lisiblement un array.
print_r(unserialize($serialized)); // On utilise « print_r » pour une bonne raison.
echo '</pre>';
?>
On sait donc serializer et deserializer. Apprenons donc à nous servir de cela, en passant par quelques exemples concrets.
Pour commencer, serializons un array de profondeur inconnue.
<?php
$srzed = serialize($array_inconnu);
?>
L'enregistrement dans un fichier
On va garder cet array pour la suite. Commençons par tenter de l'enregistrer dans un fichier.
<?php
$fh = fopen('test.txt','a+'); // Ouverture d'un fichier en lecture/écriture, en le créant s'il n'existe pas.
fwrite($fh,$srzed); // On écrit.
fclose($fh); // On ferme.
?>
Quoi de plus simple ?
Notez qu'il reste un problème : si le fichier trouve un caractère spécial ( , …), il va le traduire comme tel. Assurez-vous de vous protéger contre cela, par exemple en doublant le \.
L'utilisation de serialize en barre d'adresse
Un autre exemple, à présent : la transmission via GET. En effet, une chaîne peut facilement être transmise dans l'URL. Testons cela :
<?php
header("Location:autrepage.php?data=".$srzed); // Une simple redirection, mais avec des données GET.
exit;
?>
Ce code générera probablement… une erreur. C'est normal. Certains caractères ne sont pas supportés dans les URL ou ont un sens bien particulier, notamment ces ';' qui sont si présents dans un array serializé… Mais une solution simple à cela est alors d'utiliser urlencode, qui encode ces caractères non supportés ou à sens particulier, pour le passage via URL. :)
Testons à nouveau !
<?php
header("Location:autrepage.php?data=".urlencode($srzed)); // Une simple redirection, mais avec des données GET.
exit;
?>
Testons à présent l'insertion en BDD, sans doute le plus attractif pour beaucoup. Le principe reste le même : on utilise serialize sur l'array et l'on enregistre. Mais il faut juste ajouter l'étape de protection des données. Certains utilisent mysql_real_escape_string, d'autres addslashes. Je pense qu'il s'agit de préférences, même si mysql_real_escape_stringdevrait être utilisé.
Testons :
<?php
mysql_query("INSERT INTO matable VALUES(NULL, 'serialize', '".mysql_real_escape_string($srzed)."')") or die(mysql_error());
?>
Si je vous dis, à présent, que les arrays ne sont pas le seul type à être intéressant à serializer…
<?php
class test
{
var $truc;
var $machin;
function test() // Constructeur peut aussi être appelé __construct en PHP 5.
{
$this->truc = mt_rand(1,99);
$this->machin = array('truc'=>array(5,8,7),'chose'=>5,array('t','y','u','p'));
}
}
$object = new test();
echo serialize($object);
?>
Woh. Là, encore plus de possibilités, même si le tout reste identique. Le modèle semble très proche de celui des arrays : l'objet est délimité par des accolades. Le nom de l'objet étant une chaîne de caractères, serialize précise le nombre de caractères de ce nom suivi du nombre d'éléments (propriétés) dans l'objet. On peut donc utiliser serialize sur des objets. Le nom de l'objet est conservé, ce qui permet, par exemple, de conserver des objets de diverses natures, comme un élément SimpleXML. :) La transmission d'un objet par GET, ou son stockage en fichier ou en BDD est alors… réglé !
__sleep et __wakeup
Des méthodes magiques __sleep et __wakeup peuvent être définies spécialement pour serialize et unserialize. __sleep sera appelée automatiquement lors de serialize, et fera son office AVANT la linéarisation. __wakeup sera appelée automatiquement lors d'unserialize, et fera ses opérations APRÈS la délinéarisation. Cela peut être très utile dans certains cas, comme pour une connexion à une base de données fermée par __sleep et relancée via __wakeup…
Comportement de __sleep
Si __wakeup se comporte de manière relativement normale, il n'en est pas de même pour __sleep. Vous devez en effet systématiquement renvoyer un array contenant en valeurs les noms littéraux des variables. Prenons cet exemple :
<?php
class test
{
var $truc;
function test() // Constructeur peut aussi être appelé __construct en PHP 5.
{
$this->truc = mt_rand(1,24);
}
function __sleep()
{
$this->truc *= 7;
$this->truc -= 3;
return array('truc'); // On renvoie un array contenant le nom littéral de la variable, c.-à-d. 'truc' !
}
function __wakeup()
{
$this->truc += 3;
$this->truc /= 7;
}
}
$object = new test();
echo '<pre>';
print_r($object); // Sert à afficher de façon propre les propriétés d'un objet.
echo '</pre>';
$mid = serialize($object);
echo $mid;
$last = unserialize($mid);
echo '<pre>';
print_r($last); // Sert à afficher de façon propre les propriétés d'un objet.
echo '</pre>';
?>
Le return array('truc'); renvoie en réalité…
Citation : Affichage
O:4:"test":1:{s:4:"truc";i:53;}
En réalité, __sleep « met » l'array renvoyé dans l'objet en cours de linéarisation. Attention, car si vous ne faites pas ainsi, serialize linéarisera la valeur NULL, ce qui donnera 'N'… Mais si vous regardez bien, ce comportement particulier nous permettrait de modifier une variable pour en « camoufler » la valeur réelle, ou encore de ne renvoyer que certaines propriétés de l'objet (il n'est parfois pas nécessaire voire inutile de linéariser et enregistrer certaines propriétés)… Ce comportement atypique peut donc nous permettre d'agir en conséquence. :)
Que pourrait-il nous rester à apprendre ?
Utilisation plus atypique
Vous allez me dire qu'un array serializé est inutilisable dans une requête SELECT. En effet, ça ne semble pas vraiment commode, mais… c'est possible !
Rappelons-nous la structure d'un array linéarisé : a:(nombre d'éléments):{(valeurs)} ainsi que celui d'un élément d'array linéarisé : (type élément)[:(nombre de caractères si c'est une chaîne)]:(valeur);
En sachant cela, on sait que pour rechercher l'entier 4, on va rechercher en fait la chaîne 'i:4;'. En partant de là, on peut facilement faire de petites recherches en SQL… Un exemple simple et typique (et non optimisé à mon avis : amateurs de SQL, les suggestions sont bienvenues) :
SELECT * FROM TABLE WHERE `champ` LIKE '%i:4;%'
Et voilà comment on peut rechercher les entrées dont le champ `champ` contient l'entier 4. serialize, c'est magique. :)
Comme vous avez pu le constater, le duo serialize/unserialize est vraiment puissant, permettant ainsi de grandes choses pour une action aussi simple.
Juste une chose : les ressources ne sont pas linéarisables via serialize. La fonction transtypera la ressource en entier (donc 0), et donnera donc i:0;…
Mais entraînez-vous, par exemple, à développer une classe SQL manipulant les objets __sleep et __wakeup pour conserver des données de classes d'une page à l'autre.
Un autre exemple (dont j'abuse personnellement) est celui de linéariser un array pour le transmettre via GET (je l'utilise pour les fichiers PHP générant une image via GD) en passant par urlencode.
Et voilà, vous savez à présent pas mal de choses sur la (dé)linéarisation via serialize/unserialize !