PHP 5.3 : Nouveautes de la SPL (partie 1)

17 novembre 2008SPL, php, php-5.3
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Les exemples correspondant à ce point se trouvent dans le répertoire "spl".

Qui dit nouvelle version de PHP dit généralement nouveautés et amélioration pour la SPL (Standard PHP Library).
PHP 5.3 ne déroge pas à la règle, et voit l'apparition de plusieurs nouvelles classes :

  • Des itérateurs au niveau du système de fichier : FilesystemIterator et GlobIterator,
  • Et de nombreuses structures de données : files, piles, listes, ...

Encore une fois[1], pour éviter que cette article ne soit trop long, j’ai choisi de le découper en deux parties : une que je publie aujourd’hui (lundi 17/11), et l’autre qui sera pour demain, mardi 18 novembre 2008.

Sommaire de cette première partie :


GlobIterator

Le premier itérateur ajouté à PHP 5.3 est le GlobIterator, qui permet de parcourir des fichiers dont les noms correspondent à un masque -- comme le fait la fonction glob.

Par exemple, pour lister et parcourir tous les fichiers d'extension << .php >> dans le répertoire courant :

echo '<pre>';

$motif = __DIR__ . '/*.php';
$fichiers = new GlobIterator($motif);
foreach ($fichiers as $fichier) {
    // $fichier est une instance  de SplFileInfo
    echo $fichier->getPathname() . ' : ' . number_format($fichier->getSize(), 0, '.', ',') . " octets\n";
}

echo '</pre>';

Ce qui donne, au moment où j'écris ceci :

/home/php/php53/spl/FilesystemIterator-5.3-1.php : 1,540 octets
/home/php/php53/spl/GlobIterator-5.3-1.php : 296 octets
/home/php/php53/spl/SplDoublyLinkedList-5.3-1.php : 138 octets
/home/php/php53/spl/SplMaxHeap-5.3-1.php : 179 octets
/home/php/php53/spl/SplMinHeap-5.3-1.php : 179 octets
/home/php/php53/spl/SplMinHeap-5.3-2.php : 494 octets
/home/php/php53/spl/SplMinHeap-5.3-3.php : 883 octets
/home/php/php53/spl/SplPriorityQueue-5.3-1.php : 301 octets
/home/php/php53/spl/SplQueue-5.3-1.php : 755 octets
/home/php/php53/spl/SplStack-5.3-1.php : 718 octets

Libre à vous d'imaginer des motifs plus complexes, bien évidemment ^^

L'intérêt par rapport à travailler avec la fonction glob ? Vous n'obtenez pas en sortie uniquement des noms de fichiers, mais directement des instances de SplFileInfo, qui regorgent d'informations sur les fichiers que vous souhaitez manipuler !


FilesystemIterator

La seconde classe ajoutée est -- encore -- un itérateur au niveau du système de fichiers : FilesystemIterator.
Comme son nom l'indique, il permet de parcourir le FS. Par exemple :

$fi = new FilesystemIterator(__DIR__, FilesystemIterator::KEY_AS_PATHNAME);

echo '<pre>';
foreach ($fi as $key => $fichier) {
    // $fichier est une instance  de SplFileInfo
    echo $key . '  =>  ';
    echo number_format($fichier->getSize(), 0, '.', ',') . " octets\n";
}
echo '</pre>';

Nous renvoi la sortie suivante :

/home/php/php53/spl/SplMinHeap-5.3-1.php  =>  179 octets
/home/php/php53/spl/.svn  =>  4,096 octets
/home/php/php53/spl/SplQueue-5.3-1.php  =>  755 octets
/home/php/php53/spl/SplMinHeap-5.3-2.php  =>  494 octets
/home/php/php53/spl/SplDoublyLinkedList-5.3-1.php  =>  138 octets
/home/php/php53/spl/SplPriorityQueue-5.3-1.php  =>  301 octets
/home/php/php53/spl/SplMinHeap-5.3-3.php  =>  883 octets
/home/php/php53/spl/SplStack-5.3-1.php  =>  718 octets
/home/php/php53/spl/GlobIterator-5.3-1.php  =>  296 octets
/home/php/php53/spl/FilesystemIterator-5.3-1.php  =>  1,540 octets
/home/php/php53/spl/SplMaxHeap-5.3-1.php  =>  179 octets


SplDoublyLinkedList

La première nouvelle structure de données dont je parlerai ici est la liste doublement chainée, nommée fort logiquement SplDoublyLinkedList.
Elle est utilisée comme classe de base pour plusieurs autres structures de données.

Un exemple :

$l = new SplDoublyLinkedList();

$l->push(10);
$l->push(5);
$l->push(50);

var_dump('count : ' . count($l));

foreach ($l as $element) {
    var_dump($element);
}

var_dump('count : ' . count($l));

Ajoutons des éléments à l'aide de la méthode push, et parcourons la liste en utilisant le mot-clef foreach, tout en affichant le résultat :

string 'count : 3' (length=9)
int 10
int 5
int 50
string 'count : 3' (length=9)

On notera que parcourir une SplDoublyLinkedList ne la vide pas.
Il en sera de même pour les classes étendant celle-ci.


SplMinHeap et SplMaxHeap

Passons maintenant aux tas à priorité : SplMinHeap et SplMaxHeap, qui étendent toutes deux la classe SplHeap, que je ne présenterai pas en détails, en particulier parce qu'elle est définie comme abstract, et ne peut donc être utilisée directement.
Comme leurs noms l'indiquent, ces classes fonctionnent avec un mécanisme de priorisation.

Par exemple, insérons des éléments désordonnés dans une SplMinHeap, et dépilons-les à l'affichage :

$h = new SplMinHeap();

$h->insert(60);
$h->insert(10);
$h->insert(20);
$h->insert(50);
$h->insert(5);
$h->insert(8);

var_dump('count : ' . count($h));

foreach ($h as $element) {
    var_dump($element);
}

var_dump('count : ' . count($h));

Bien que désordonnés lors de leur insertion, nos éléments apparaissent ordonnés en sortie, les plus petits en premier (puisque nous avons utilisé une instance de SplMinHeap ; nous aurions obtenu exactement le résultat inverse avec une instance de SplMaxHeap) :

string 'count : 6' (length=9)
int 5
int 8
int 10
int 20
int 50
int 60
string 'count : 0' (length=9)

Notez que ces classes héritent de SplHeap : parcourir ces structures de données les vide, donc !
(Nous dépilons au fur et à mesure du parcours)

Passons maintenant à un cas où nous ne souhaitons plus travailler avec des entiers (que nous savons, tout comme PHP, très bien comparer), mais avec des objets incluant une notion de poids.

Par exemple, utilisons des objets constitués d'une chaîne de caractères (propriété str), et d'un entier faisant office de poids (propriété poids -- vous l'aurez deviné ^^), et ajoutons quelques instances d'objets de cette classe à une SplMinHeap :

class A {
    public $str, $poids;
    public function __construct($s, $p) {
        $this->str = $s;
        $this->poids = $p;
    }
}

$h = new SplMinHeap();

$h->insert(new A('Why are we here?', 60));
$h->insert(new A('Genesis (The Making And The Fall Of Man)', 10));
$h->insert(new A('Nostradamus', 20));
$h->insert(new A('Sands of Time', 50));
$h->insert(new A('Lost in the Ice', 5));
$h->insert(new A('Mein Herz Brennt', 8));

foreach ($h as $element) {
    var_dump($element);
}

L'affichage obtenu lors du parcours est le suivant :

object(A)[3]
  public 'str' => string 'Genesis (The Making And The Fall Of Man)' (length=40)
  public 'poids' => int 10
object(A)[6]
  public 'str' => string 'Lost in the Ice' (length=15)
  public 'poids' => int 5
object(A)[7]
  public 'str' => string 'Mein Herz Brennt' (length=16)
  public 'poids' => int 8
object(A)[4]
  public 'str' => string 'Nostradamus' (length=11)
  public 'poids' => int 20
object(A)[5]
  public 'str' => string 'Sands of Time' (length=13)
  public 'poids' => int 50
object(A)[2]
  public 'str' => string 'Why are we here?' (length=16)
  public 'poids' => int 60

Les données ont été triées, ce qui était prévisible puisque nous utilisons une pile avec priorité... Mais en fonction de l'attribut str, et non de leurs poids :-(

Pour résoudre ce problème, il nous faut créer une nouvelle classe, héritant de SplMinHeap pour en reprendre toutes les fonctionnalités, mais surchargeant la méthode compare, qui est celle responsable du tri des éléments poussés sur le tas : en la rendant capable de comparer correctement nos objets, nous obtiendrons du tas le comportement que nous souhaitons :

class A {
    public $str, $poids;
    public function __construct($s, $p) {
        $this->str = $s;
        $this->poids = $p;
    }
}

class MySplMinHeap extends SplMinHeap {
    // Result of the comparison, positive integer if value1  is lower than value2 ,
    // 0 if they are equal, negative integer otherwise.
    public function compare(A $a, A $b) {
        if ($a->poids > $b->poids) {
            return -1;
        } else if ($a->poids < $b->poids) {
            return 1;
        }
        return 0;
    }
}

$h = new MySplMinHeap();

$h->insert(new A('Why are we here?', 60));
$h->insert(new A('Genesis (The Making And The Fall Of Man)', 10));
$h->insert(new A('Nostradamus', 20));
$h->insert(new A('Sands of Time', 50));
$h->insert(new A('Lost in the Ice', 5));
$h->insert(new A('Mein Herz Brennt', 8));

foreach ($h as $element) {
    var_dump($element);
}

Et en sortie, ces quelques titres de chansons[2]de quelques uns de mes groupes musicaux favoris apparaissent, triés par leur champ poids, comme initialement souhaité :

object(A)[6]
  public 'str' => string 'Lost in the Ice' (length=15)
  public 'poids' => int 5
object(A)[7]
  public 'str' => string 'Mein Herz Brennt' (length=16)
  public 'poids' => int 8
object(A)[3]
  public 'str' => string 'Genesis (The Making And The Fall Of Man)' (length=40)
  public 'poids' => int 10
object(A)[4]
  public 'str' => string 'Nostradamus' (length=11)
  public 'poids' => int 20
object(A)[5]
  public 'str' => string 'Sands of Time' (length=13)
  public 'poids' => int 50
object(A)[2]
  public 'str' => string 'Why are we here?' (length=16)
  public 'poids' => int 60

Juste pour la route, avant de terminer, jetons un coup d'oeil sur la classe SplMaxHeap, qui fonctionne exactement de la même manière que SplMinHeap, en triant dans l'ordre inverse :

$h = new SplMaxHeap();

$h->insert(60);
$h->insert(10);
$h->insert(20);
$h->insert(50);
$h->insert(5);
$h->insert(8);

foreach ($h as $element) {
    var_dump($element);
}

Avec la sortie suivante :

int 60
int 50
int 20
int 10
int 8
int 5

Rien de très surprenant, donc ^^

Et ici encore, libre à vous de définir une classe héritant de SplMaxHeap, et surchargeant à votre convenance la méthode compare, si vous souhaitez travailler avec des types de données plus riches que les int utilisés ici.


SplPriorityQueue

Passons maintenant à la classe SplPriorityQueue : une file avec gestion de priorité.

Le principe est simple -- c'est celui d'une queue : vous insérez des éléments à un bout de la file, et vous les récupérez à l'autre bout, en FIFO...
Et ce avec une gestion de priorité : pour chaque élément inséré avec la méthode insert, vous indiquez en second paramètre son poids :

$q = new SplPriorityQueue();

$q->setExtractFlags(SplPriorityQueue::EXTR_BOTH);

$q->insert('Mr A', 10);
$q->insert('Mr B', 30);
$q->insert('Mr C', -1);
$q->insert('Mr D', 5);

var_dump(count($q));

// Attention : le parcours enlève les éléments de la queue !
foreach ($q as $element) {
    var_dump($element);
}

var_dump(count($q));

Ce qui donne à l'affichage la sortie suivante :

int 4
array
  'data' => string 'Mr B' (length=4)
  'priority' => int 30
array
  'data' => string 'Mr A' (length=4)
  'priority' => int 10
array
  'data' => string 'Mr D' (length=4)
  'priority' => int 5
array
  'data' => string 'Mr C' (length=4)
  'priority' => int -1
int 0

A vous de vous amuser avec un exemple plus complet, avec des insertions et des extractions alternées ; vous retrouverez, en toute logique, le comportement attendu pour une file.
(Et vous noterez au passage que le parcours vide la file.)


Voila pour aujourd'hui ^^

Le point à retenir, à mon avis, serait : utilisez la SPL ! Elle regroupe nombre de classes intéressantes... Et lire le manuel quelques minutes vous coûtera certainement nettement moins cher que de ré-inventer la roue à chaque nouveau projet !

Et à demain pour la suite des nouveautés que PHP 5.3 apporte à la Standard PHP Library ;-)


Notes

[1] Pour le troisième début de semaine de suite, devrais-je dire ^^

[2] Je sens déjà venir les questions, donc, pour simplifier, répondons à l'avance : "Lost in the Ice" : Cf album "The Missing Link", de Rage -- "Mein Herz Brennt" : Cf ablum "Mutter" de "Rammstein" -- "Genesis (The Making And The Fall Of Man)" : Cf ablum "Black Hand Inn" de "Running Wild" -- "Nostradamus" et "Sands of Time" : Cf ablum "Nostradamus" de "Judas Priest" -- "Why are we here?" : Cf ablum "Intermission" de "Stratovarius",