Les itérateurs en PHP

Publié le 28 février 2019 par Abouchard

Les itérateurs existent depuis bien longtemps dans les langages de programmation. J’utilisais déjà des itérateurs en C++ il y a 20 ans, et ce n’était pas un truc nouveau. En PHP, les itérateurs sont apparus avec la version 5 (en 2004).

Les itérateurs servent à parcourir facilement des ensembles de données. En PHP, cela se traduit par l’utilisation du foreach sur quelque chose qui n’est pas un tableau, mais qu’on va pouvoir parcourir comme un tableau.

Exemple d’itérateur

Prenons un exemple concret. Disons que nous voulons lire un fichier ligne par ligne. Un moyen de le faire est d’utiliser la fonction file() pour récupérer un tableau, qui contiendra un élément pour chaque ligne du fichier.

$content = file('chemin/vers/fichier.txt');
foreach ($content as $line) {
// on a une ligne du fichier dans la variable $line
}

Ça fonctionne très bien, c’est facile à lire, tout le monde comprend très vite ce que fait ce code. Sauf que si on veut lire un très gros fichier, on va vite dépasser la limite de mémoire utilisable en construisant le tableau.
La solution, c’est d’utiliser les fonctions de plus bas niveau, pour ouvrir un descripteur sur le fichier, puis lire le fichier ligne par ligne.

$fd = fopen('chemin/vers/fichier.txt', 'r');
while (!feof($fp)) {
$line = fgets($fd);
// on a une ligne du fichier dans la variable $line
}
fclose($fd);

Ce code est beaucoup moins gourmand en ressources, mais il faut reconnaître qu’il est moins lisible.
C’est là que les itérateurs interviennent. Ils permettent de garder une écriture simple du code, avec une utilisation plus fine des ressources.

La bibliothèque standard de PHP fournit l’objet SplFileObject, qui − comme tous les objets qui implémentent l’interface Iterator − peut être parcouru comme un tableau.
Voici un exemple équivalent au code précédent :

$iterator = new SplFileObject('chemin/vers/fichier.txt');
foreach ($iterator as $line) {
// on a une ligne du fichier dans la variable $line
}

Là, on a tous les avantages : non seulement le fichier est lu une ligne à la fois, ce qui permet de lire un gros fichier sans problème, mais le code est très simple à écrire et à comprendre.
L’objet de type SplFileObject étant un itérateur, on peut boucler dessus avec foreach, comme si c’était un tableau dont on voulait parcourir les éléments.

Pour la petite histoire, on peut même récupérer le numéro de ligne qui est lu (en fait, on récupère l’équivalent de la clé dans un tableau) :

$iterator = new SplFileObject('chemin/vers/fichier.txt');
foreach ($iterator as $lineNum => $line) {
// on a une ligne du fichier dans la variable $line
// on a le numéro de ligne dans la variable $lineNum
}

Un second exemple

Imaginons que l’on souhaite maintenant parcourir tous les fichiers contenus dans un répertoire. Encore une fois, il y a plusieurs manières de faire.

La méthode la plus simple est de récupérer la liste de tous les fichiers du répertoire grâce à la fonction glob(), puis de parcourir cette liste :

$list = glob('chemin/vers/repertoire/*');
foreach ($list as $file) {
// on a un nom de fichier dans la variable $file
}

Mais si le répertoire contient un très grand nombre de fichiers, cela peut encore une fois dépasser la mémoire utilisable. Dans ce cas, on peut utiliser les fonctions de bas niveau pour récupérer les noms de fichiers un par un :

$dir = opendir('chemin/vers/repertoire');
while (($file = readdir($dir)) !== false) {
// on a un nom de fichier dans la variable $file
}
closedir($dir);

On peut voir que la logique est très similaire à celle utilisée dans le premier exemple, pour lire un fichier une ligne à la fois.
Pour écrire la même chose en utilisant un itérateur, on peut faire appel à l’objet FilesystemIterator.

$iterator = new FilesystemIterator('chemin/vers/repertoire');
foreach ($iterator as $file) {
// on a un nom de fichier dans la variable $file
}

De nouveau, l’intérêt est évident. Le code est facile à lire tout en étant très efficace dans son utilisation de la mémoire.

Chaîner les itérateurs

L’un des avantages des itérateurs, c’est qu’on peut les chaîner. Ou plutôt, on peut utiliser un itérateur qui va agir en surcouche d’un autre itérateur, faisant des traitements supplémentaires ou en limitant les actions que l’on pourra effectuer sur l’itérateur d’origine.

On va illustrer ça avec l’objet LimitIterator, dont le constructeur prend en paramètre un autre itérateur, dont on veut restreindre les itérations.

Reprenons le premier exemple, mais cette fois-ci on ne veut lire que la quatrième et la cinquième ligne du fichier :

$fileIterator = new SplFileObject('chemin/vers/fichier.txt');
$iterator = new LimitIterator($fileIterator, 3, 2);
foreach ($iterator as $line) {
// on a une ligne du fichier dans la variable $line
}

Encore une fois, le code se retrouve très simple à comprendre. Et surtout, on peut passer un itérateur en paramètre à des fonctions/méthodes, qui se contenteront de boucler dessus, sans qu’elles aient besoin de savoir si on a fourni un itérateur “brut” ou si on l’a encapsulé dans un autre itérateur pour agir dessus.

Développer son propre itérateur

Jusqu’ici, on a vu comment manipuler des itérateurs fournis par PHP. Il en existe d’ailleurs un certain nombre.
Mais écrire un objet qui se comporte comme un itérateur est plus compliqué. Il faut que l’objet implémente l’interface Iterator, qui impose les méthodes suivantes :

  • current() : Retourne l’élément courant, sur lequel l’itérateur est actuellement positionné.
  • key() : Retourne la clé de l’élément courant.
  • next() : Fait avancer l’itérateur à l’élément suivant.
  • rewind() : Fait revenir l’itérateur au premier élément.
  • valid() : Retourne true s’il reste des éléments sur lesquels boucler.

Pour illustrer à quoi servent ces méthodes, voici le premier exemple réécrit d’une manière plus explicite (et plus verbeuse) :

// on créé l'itérateur
$iterator = new SplFileObject('chemin/vers/fichier.txt');
// on (ré-)initialise l'itérateur
$iterator->rewind();
// on boucle tant qu'on n'est pas arrivé au bout de l'itérateur
while ($iterator->valid()) {
// on récupère le numéro de ligne dans la variable $lineNum
$lineNum = $iterator->key();
// on récupère une ligne du fichier dans la variable $line
$line = $iterator->current();
// on dit à l'itérateur de passer à la ligne suivante
$iterator->next();
}

Voici le code d’un objet qui fait la même chose que l’objet SplFileObject, servant donc à lire le contenu d’un fichier ligne par ligne :

class MonFichier implements Iterator {
/** Le chemin du fichier. */
private $_path = null;
/** Le descripteur ouvert sur le fichier. */
private $_file = null;
/** Le contenu de la ligne courante. */
private $_line = null;
/** Le numéro de la ligne courante. */
private $_index = -1;
/**
* Constructeur.
* @param string $path Le chemin vers le fichier.
*/
public function __construct($path) {
$this->_path = $path;
}
/** Destructeur. Ferme le descripteur sur le fichier. */
public function __destruct() {
fclose($this->_file);
}
/** Rewind : Ouvre le fichier et lit sa première ligne. */
public function rewind() {
$this->_file = fopen($this->_path, 'r');
$this->next();
}
/**
* Current : Retourne la ligne courante.
* @return string Le contenu de la ligne courante.
*/
public function current() {
return $this->_line;
}
/**
* Key : Retourne le numéro de la ligne courante.
* @return int Le numéro de la ligne courante.
*/
public function key() {
return $this->_index;
}
/** Next : Avance à la ligne suivante. */
public function next() {
$this->_line = trim(fgets($this->_file));
$this->_index++;
}
/**
* Valid : Indique si on peut encore boucler.
* @return bool True si on peut encore boucler.
*/
public function valid() {
return !feof($this->_file);
}
}

Bon, c’est un code d’exemple, sans aucune gestion d’erreur. Mais normalement, vous devriez comprendre facilement le principe.
Comme vous pouvez le voir, l’écriture d’un itérateur est assez contraignante et nécessite pas mal de code, même pour un exemple simple.

En tout cas, son utilisation reste aussi simple que ce qu’on a vu précédemment :

$iterator = new MonFichier('chemin/vers/fichier.txt');
foreach ($iterator as $lineNum => $line) {
// on a une ligne du fichier dans la variable $line
// on a le numéro de ligne dans la variable $lineNum
}

À noter que si vous avez un objet qui contient un itérateur parmi ses attributs, vous pouvez facilement en faire lui-même un itérateur, et implémentant l’interface IteratorAggregate et en ajoutant une méthode getIterator() qui retourne l’itérateur sous-jacent.

class MonAutreFichier implements IteratorAggregate {
/** Iterateur sur le fichier. */
private $_fileIterator = null;
/**
* Constructeur.
* @param string $path Chemin vers le fichier.
*/
public function __construct($path) {
$this->_fileIterator = new SplFileObject($path);
}
/**
* Retourne l'iterateur ouvert sur le fichier.
* @return Traversable L'itérateur ouvert.
*/
public function getIterator() {
return $this->_fileIterator;
}
}

Cette écriture peut être très pratique (d’autant plus si vous utilisez les nombreux itérateurs proposés par PHP), mais la plupart du temps vous voudrez contrôler les appels qui sont faits sur l’itérateur, pour l’encapsuler complètement ; et pour cela, vous serez bien forcé de réimplémenter toutes les méthodes vues précédemment.

Avantages et inconvénients des itérateurs

Alors, les itérateurs, quand est-ce qu’ils sont utiles ? Pour commencer, dans les situations évoquées plus haut, où on veut garder la faciliter d’écriture d’un foreach sans risquer d’exploser la mémoire disponible.
En allant plus loin, on peut généraliser leur usage à tous les cas pour lesquels on a besoin de boucler sur des données. On bénéficie alors de la souplesse qu’offrent les itérateurs, notamment la possibilité de les chaîner de manière transparente pour le code qui va les manipuler.

Mais je relèverai toutefois deux bémols.

Tout d’abord, si votre code n’est pas massivement orienté objet, vous risquez d’induire les autres développeurs en erreur. En PHP, on a l’habitude de manipuler des tableaux ; quand on voit qu’une variable est utilisée dans un foreach, on a le réflexe de penser que c’est un tableau et on est donc tenté de l’utiliser comme tel, en lui ajoutant des éléments ou en utilisant des fonctions dédiées (array_pop(), array_slice(), etc.).

Et comme on l’a vu, écrire ses propres itérateurs peut être assez laborieux. Honnêtement, les itérateurs proposés par PHP sont assez pratiques, mais on se met à soupirer dès qu’il s’agit d’en écrire un soi-même.
C’est pour cette raison qu’ont été créés les générateurs, qui sont arrivés dans la version 5.5 de PHP (en 2013), mais qui existaient déjà dans d’autres langages. Ils permettent de créer des itérateurs avec une syntaxe bien plus simple − et surtout bien moins verbeuse − une fois qu’on a compris comment ils fonctionnent.

Mais je vais revenir sur les générateurs dans un prochain article