PHP 5.3 : Nouveautes de la SPL (partie 2)

18 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".

Voici la seconde partie de cet article traitant des nouveautés que PHP 5.3 ajoute à la SPL, la Standard PHP Library -- la première partie ayant été publiée hier matin.

Sommaire de cette seconde partie :


SplQueue

En restant dans le même registre, mais sans avoir de notion de priorisation cette fois, passons à la classe SplQueue, qui permet de gérer une queue des plus standard[1].

Cette fois-ci, je présenterai un exemple un peu plus long, histoire d'illustrer un peu mieux ce que donne l'utilisation de ces nouveaux ajouts à la SPL.
Commençons par définir une fonction affichant le contenu d'une queue :

function outputQueue($q) {
    echo '<hr />File :';
    foreach ($q as $element) {
        var_dump($element);
    }
}

Nous pourrons appeler cette fonction aussi souvent que voulu sur une instance de SplQueue, parce que le parcours d'une queue non priorisée ne la vide pas -- contrairement à ce qu'il se passait pour plusieurs des classes vues jusqu'à présent (c'est justement pour cela que je prends celle-ci pour cet exemple : il est plus facile de comprendre ce qu'il se passe si on peut effectuer des sorties écran sans vider la file à chaque fois ^^ ).
Ceci est dû au fait que SplQueue étend SplDoublyLinkedList ; elle reprend donc le comportement de cette première structure de données.

Commençons par déclarer une instance de SplQueue, et ajoutons-lui quelques éléments :

  • Un nombre entier,
  • une liste,
  • et un objet.

On notera que l'on peut utiliser la méthode enqueue pour ajouter des éléments en fin de file, et que l'on peut aussi utiliser l'opérateur [] : SplQueue implémente l'interface ArrayAccess.

$q = new SplQueue();

$q[] = 10;
$q->enqueue(array(10, 'glop', 30));
$q[] = new stdClass();

outputQueue($q);

En affichant la file, nous obtenons ceci :

File :
int 10
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
object(stdClass)[2]

La queue avec laquelle nous travaillons contient bien trois entrées, et la première ajoutée est première à apparaitre lors du parcours (FIFO, donc).

Ajoutons un élément de plus :

$q[] = 'Hello, World!';

outputQueue($q);

Il vient s'ajouter en fin de file :

File :
int 10
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
object(stdClass)[2]
string 'Hello, World!' (length=13)

Et si maintenant nous voulons extraire une entrée de la file, faisons appel à la méthode dequeue :

$a = $q->dequeue();
echo '<hr />Elément "dequeued" :';
var_dump($a);

outputQueue($q);

L'élément à sortir est le premier qui avait été ajouté, et la file ne contient plus que trois entrées -- les trois ajoutées en dernier :

Elément "dequeued" :
int 10

File :
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
object(stdClass)[2]
string 'Hello, World!' (length=13)

Passons à l'extraction d'un second élément :

$a = $q->dequeue();
echo '<hr />Elément "dequeued" :';
var_dump($a);

outputQueue($q);

Plus que deux éléments dans la file, et c'est toujours en tête de file que nous extrayons les données :

Elément "dequeued" :
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30

File :
object(stdClass)[2]
string 'Hello, World!' (length=13)

Et si nous continuons :

$a = $q->dequeue();
echo '<hr />Elément "dequeued" :';
var_dump($a);

outputQueue($q);

Presque vide :

Elément "dequeued" :
object(stdClass)[2]

File :
string 'Hello, World!' (length=13)

Encore une fois :

$a = $q->dequeue();
echo '<hr />Elément "dequeued" :';
var_dump($a);

outputQueue($q);

Nous avons récupéré le tout dernier élément inséré, et notre file est maintenant vide :

Elément "dequeued" :
string 'Hello, World!' (length=13)

File :

Et que se passe-t-il si nous essayons de dé-filer encore une fois :

// La file est vide, à présent
$a = $q->dequeue();  // RuntimeException: Can't shift from an empty datastructure

Lorsque nous essayons de dé-filer un élément d'une SplQueue vide, nous prenons une RuntimeException :

splqueue-dequeue-exception-when-empty.png

Pensez-y, et n'hésitez pas à utiliser les méthodes count pour savoir combien d'éléments il reste dans votre file, ou isEmpty pour vous assurer qu'elle n'est pas vide !
Par exemple :

if ($q->isEmpty()) {
    echo '<hr />';
    echo 'La file est vide !';
}


SplStack

Passons maintenant à la classe SplStack, qui implémente fort logiquement une pile LIFO.
Ici aussi, cette classe hérite de SplDoublyLinkedList, et peut donc être parcourue sans que cela ne la vide, ce qui va nous permettre de rentrer un peu dans les détails sur un exemple :

Tout d'abord, comme précédemment, une fonction pour afficher le contenu de la pile :

function outputStack($q) {
    echo '<hr />Pile :';
    foreach ($q as $element) {
        var_dump($element);
    }
}

Et maintenant, instancions la classe SplStack, et ajoutons lui quelques éléments.
Nous pouvons soit utiliser la méthode push, soit passer par l'opérateur [], puisque SplStack implémente @@ArrayAccess@ :

$q = new SplStack();

$q[] = 10;
$q->push(array(10, 'glop', 30));
$q[] = new stdClass();

outputStack($q);

Notre pile nouvellement créée a le contenu suivant :
Les éléments apparaissent à l'affichage dans l'ordre inverse de leurs insertions : LIFO

Pile :
object(stdClass)[2]
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
int 10

Ajoutons un élément :

$q[] = 'Hello, World!';

outputStack($q);

Notre nouvel élément est inséré en dernier ; il apparait donc en premier lors du parcours de la pile :

Pile :
string 'Hello, World!' (length=13)
object(stdClass)[2]
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
int 10

Et si nous commençons à extraire des éléments de la pile, avec la méthode pop :

$a = $q->pop();
echo '<hr />Elément "popé" :';
var_dump($a);

outputStack($q);

Le premier élément à sortir est le dernier inséré :

Elément "popé" :
string 'Hello, World!' (length=13)

Pile :
object(stdClass)[2]
array
  0 => int 10
  1 => string 'glop' (length=4)
  2 => int 30
int 10

Je vous dispense du dépilage des trois éléments suivants -- vous aurez déjà compris dans quel ordre ils sortiront, j'espère ;-)


SplFixedArray

Avant de terminer, pour la route, une petite dernière ^^
(Dont j'ai à peine entendu parler, je dois avouer : elle n'est à priori pas documentée sur php.net, et je n'ai pas le souvenir de l'avoir vue mentionnée ailleurs que dans un article mis en ligne il y a quelques jours sur le DeveloperWorks d'IBM)

La classe SplFixedArray permet de représenter des tableaux répondant à deux conditions :

  • SplFixedArray ne permet de stocker des listes que de taille fixe,
  • Et seuls des nombres entiers peuvent être utilisés comme clefs de ces tableaux.

Finalement, on s'éloigne des tableaux associatifs auxquels nous sommes habitués avec array, et nous rapprochons plus de la notion de tableaux à laquelle nous pouvions être habitués avec des langages tels C[2].


SplFixedArray : Principes de base

L'idée de la classe SplFixedArray est que la taille de la liste doit être précisée lors de sa création :

$arr = new SplFixedArray(4);

Il est ensuite possible d'ajouter des éléments, en utilisant des nombres entiers, strictement inférieurs au nombre indiqué à la création de la SplFixedArray :

$arr[0] = 'première chaine';
$arr[1] = 'seconde chaine';
$arr[2] = 'troisième chaine';
$arr[3] = 'quatrième chaine';
var_dump($arr);

Et le var_dump nous donnera en sortie :

object(SplFixedArray)[1]
  string 'première chaine' (length=16)
  string 'seconde chaine' (length=14)
  string 'troisième chaine' (length=17)
  string 'quatrième chaine' (length=17)

Quant au parcours, il se fait à l'aide de foreach, sans différence par rapport à ce à quoi nous sommes habitués :

foreach ($arr as $key => $value) {
    var_dump($key, $value);
}

Qui donne :

int 0
string 'première chaine' (length=16)
int 1
string 'seconde chaine' (length=14)
int 2
string 'troisième chaine' (length=17)
int 3
string 'quatrième chaine' (length=17)

Sans surprise, la classe SplFixedArray implémente les interfaces suivantes :

On peut donc, globalement, l'utiliser en conservant les habitudes acquises avec les array.


A noter

Deux points sont à noter lorsque l'on souhaite travailler avec la classe SplFixedArray.

Tout d'abord, si vous essayez de travailler avec un indice supérieur à la contenance de la liste, vous obtiendrez une RuntimeException.
Même chose si vous souhaitez utiliser autre chose qu'un entier comme clef pour un des éléments d'une instance de SplFixedArray, ou si vous voulez ne pas spécifier d'indice, en croyant ajouter un élément en fin de liste.

Cela signifie que cette portion de code :

$arr = new SplFixedArray(4);
$arr[0] = 'première chaine';
$arr[1] = 'seconde chaine';
$arr[2] = 'troisième chaine';
$arr[3] = 'quatrième chaine';
$arr[4] = 'cinquième chaine';
var_dump($arr);

Et celle-ci :

$arr = new SplFixedArray(4);
$arr[0] = 'première chaine';
$arr[] = 'seconde chaine';
$arr[2] = 'troisième chaine';
$arr[3] = 'quatrième chaine';
var_dump($arr);

Léveront toutes deux une exception de la forme suivante :

splfixedarray-out-of-bounds.png


Notez aussi que même si la taille d'une SplFixedArray est définie à sa construction, vous pouvez la modifier ultérieurement, à l'aide de la méthode setSize, comme ceci :

$arr = new SplFixedArray(4);
$arr[0] = 'première chaine';
$arr[1] = 'seconde chaine';
$arr[2] = 'troisième chaine';
$arr[3] = 'quatrième chaine';
var_dump($arr);

$arr->setSize(6);
$arr[4] = 'cinquième chaine';
$arr[5] = 'sixième chaine';
var_dump($arr);

Ce qui donnera l'affichage suivant :

object(SplFixedArray)[1]
  string 'première chaine' (length=16)
  string 'seconde chaine' (length=14)
  string 'troisième chaine' (length=17)
  string 'quatrième chaine' (length=17)

object(SplFixedArray)[1]
  string 'première chaine' (length=16)
  string 'seconde chaine' (length=14)
  string 'troisième chaine' (length=17)
  string 'quatrième chaine' (length=17)
  string 'cinquième chaine' (length=17)
  string 'sixième chaine' (length=15)


Et au niveau des performances ?

Pour ce qui est des performances, dans un cas où vous créez souvent de nouvelles listes, ne contenant que peu d'éléments, utiliser une instance de SplFixedArray -- et donc, une instanciation de classe -- vous coûtera probablement plus cher que de travailler avec un array.

Par exemple, avec la portion de code suivante, qui créée beaucoup de petites listes, auxquelles seuls quelques éléments sont ajoutés :

define('NB_ITERATIONS', 100000);

function testSplFixedArray()
{
    for ($i=0 ; $i<NB_ITERATIONS ; $i++) {
        $arr = new SplFixedArray(4);
        $arr[0] = 'première chaine';
        $arr[1] = 'seconde chaine';
        $arr[2] = 'troisième chaine';
        $arr[3] = 'quatrième chaine';
    }
}

function testArray()
{
    for ($i=0 ; $i<NB_ITERATIONS ; $i++) {
        $arr = array();
        $arr[0] = 'première chaine';
        $arr[1] = 'seconde chaine';
        $arr[2] = 'troisième chaine';
        $arr[3] = 'quatrième chaine';
    }
}

testSplFixedArray();    // 89% du temps
testArray();            // 7% du temps

Nous obtenons le graphe de profiling suivant :

splfixedarray-bench-3.png

Nous constatons que la plus grosse partie du temps (de l'ordre de 90%) du script est passé dans la version utilisant SplFixedArray.


Par contre, si vous partez dans la direction inverse, en ne créant que peu souvent des listes, mais que leur taille est grande, travailler avec des SplFixedArray peut devenir intéressant : leur taille étant fixée à la création, il n'est pas nécessaire de les redimensionner << à la volée >>, contrairement aux array.

Par exemple, avec la portion de code suivante :

define('NB_ITERATIONS', 1000);

function testSplFixedArray()
{
    for ($i=0 ; $i<NB_ITERATIONS ; $i++) {
        $arr = new SplFixedArray(10000);
        for ($j=0 ; $j<1000 ; $j++) {
            $arr[$j] = 'glop';
        }
    }
}

function testArray()
{
    for ($i=0 ; $i<NB_ITERATIONS ; $i++) {
        $arr = array();
        for ($j=0 ; $j<10000 ; $j++) {
            $arr[$j] = 'glop';
        }
    }
}

testSplFixedArray();    // 45% du temps
testArray();            // 55% du temps

Nous arrivons au graphe de profiling reproduit ci-dessous :

splfixedarray-bench-5.png

Et nous constatons que la tendance s'inverse, en faveur de l'utilisation de SplFixedArray.


Ici encore, c'est à l'usage que les éventuels gains réels apparaitront... Vivement l'usage, donc !


Notes

[1] Oserai-je un jeu de mot sur le fait que cela ne soit pas surprenant pour une classe de la Standard PHP Library ? Peut-être pas... trop tard ^^

[2] pour ne citer que lui