Sorry English readers, this post goes to my French followers. Let me know if you would like me to translate this one. In the meantime, you can Google translate it.
Dans une carrière de développeur, nous sommes souvent amenés à travailler avec des fichiers. Que ce soit pour importer des données dans un système ou exporter des données dans un fichier il faudra ouvrir le fichier et effectuer des opérations sur chaque ligne du fichier. Il existe plusieurs façons de lire un fichier ligne par ligne en PHP mais certaines peuvent très vite devenir problématique lorsqu'il s'agit de lire des gros fichiers.
Array
Il existe plusieurs façons de lire un fichier dans un tableau PHP, mais voici celle que je vois le plus souvent dans des projets :
<?php
require_once('utils.php');
$rows = $this->read('input.txt');
foreach($rows as $row) {
// traiter les données
}
<?php
// utils.php
public function read(string $filename): array
{
$handle = fopen($filename, "r");
$rows = [];
while(!feof($handle)) {
$rows[] = fgets($handle);
}
fclose($handle);
return $rows;
}
Problèmes
Le temps d'exécution est trop long
Plus le fichier est volumineux, plus le temps d'exécution sera long. Cela est logique car PHP aura besoin de stocker un grand nombre d'éléments et donc générer un très grand tableau.
La limite de mémoire est atteinte
Lorsque le fichier est léger, il n'y a pas de question à se poser, mais lorsque celui-ci contient un gros volume de données, c'est là que ce message d'erreur apparaît :
Fatal Error: Allowed memory size of XXXXX bytes exhausted.
En effet, dans l'exemple ci-dessus, PHP stocke toutes les données du tableau en mémoire. La taille de votre tableau sera donc limitée par la quantité de mémoire allouée au script définie par la variable memory_limit
de PHP. Il m'arrive de voir la valeur de cette variable égale à memory_limit=-1
en production, ce qui signifie que PHP utilisera toute la mémoire disponible sur le serveur ! Je ne vous recommande donc pas cette pratique mais vous propose plutôt d'utiliser un Itérateur ou un Générateur.
Itérateur
Un Itérateur est un patron de conception comportemental qui permet de parcourir les éléments d'une collection de façon séquentielle. Chaque itérateur doit indiquer la façon dont il doit être traversé, et quelles valeurs seront disponibles à chaque itération.
Depuis la version 5, PHP fournit une interface Iterator
qui peut être utilisée pour créer des itérateurs personnalisées :
Iterator extends Traversable {
/* Méthodes */
abstract public current () : mixed
abstract public key () : scalar
abstract public next () : void
abstract public rewind () : void
abstract public valid () : bool
}
L'interface déclare les opérations nécessaires au parcours d'une collection :
-
current()
: donne la position actuelle -
key()
: donne la clé de l'élement actuel -
next()
: donne le prochain élément de la collection -
rewind()
: recommence la boucle depuis le début -
valid()
: vérifie si la position actuelle est valide.
Il est donc possible de créer un itérateur qui permet de parcourir un fichier ligne par ligne :
<?php
class FileIterator implements \Iterator
{
protected $fileHandle;
protected $row;
protected $lineNumber;
public function __construct(string $fileName)
{
if (!$this->fileHandle = fopen($fileName, 'r')) {
throw new \RuntimeException('Unable to open file "' . $fileName . '" ');
}
}
/**
* Revient au début du fichier.
*/
public function rewind()
{
fseek($this->fileHandle, 0);
$this->row = fgets($this->fileHandle);
$this->lineNumber = 0;
}
/**
* Vérifie que la fin du fichier n'est pas atteinte.
*/
public function valid()
{
return !feof($this->fileHandle);
}
/**
* Retourne la ligne actuelle.
*/
public function current()
{
return $this->row;
}
/**
* Retourne le numéro de la ligne actuel.
*/
public function key()
{
return $this->lineNumber;
}
/**
* Déplace le pointeur à la ligne suivante.
*/
public function next()
{
if ($this->valid()) {
$this->row = fgets($this->fileHandle);
$this->lineNumber++;
}
}
public function __destruct()
{
fclose($this->fileHandle);
}
}
La fonction read()
peut donc être simplifiée avec l'utilisation de cet itérateur :
public function read(string $filename): FileIterator
{
return new FileIterator($filename);
}
Vous pouvez ensuite parcourir l'itérateur avec l'utilisation de foreach
:
$rows = $this->read('input.txt');
foreach($rows as $row) {
// traiter les données
}
Voici l'ordre des opérations exécutées au moment où vous bouclez sur l'itérateur :
- Avant la première itération de la boucle, la méthod
rewind()
est appelée. - Avant chaque itération de la boucle, la méthode
valid()
est appelée :- Si la méthode
valid()
retourne false, la boucle est terminée. - Si la méthode
valid()
retourne true, les méthodescurrent()
etkey()
sont appelées.
- Si la méthode
- Le corps de la boucle est évalué.
- Après chaque itération de la boucle, la méthode
next()
est appelée et nous répétons les opérations à partir de l'étape 2 ci-dessus.
À la différence d'un tableau, l'itérateur consommera très peu de mémoire (<1 Mo) car PHP aura besoin de stocker uniquement la position du pointeur et la ligne actuelle du fichier. Les précédentes lignes ne seront pas gardées en mémoire.
Problème
Les itérateurs peuvent être complexes à créer en fonction du type de fichier. Bien qu'il s'agit d'une optimisation au niveau de la mémoire utilisée, le code est plus conséquent pour une simple opération de lecture d'un fichier. Voyons une façon plus simple de créer des itérateurs à l'aide des générateurs PHP.
Genérateur
Depuis la version PHP 5.5, il est aussi possible d'économiser de la mémoire en utilisant des générateurs PHP :
Un générateur permet de parcourir les éléments d'une collection sans avoir à construire un tableau en mémoire pouvant conduire à dépasser la limite de la mémoire ou nécessiter un temps important pour sa génération.
En réalité, un générateur est un objet PHP qui implémente l'interface Iterator
. Il permet de simplifier la création des itérateurs en écrivant une seule fonction chargée de construire le tableau, sans se soucier de toutes les autres fonctions de l'itérateur permettant d’obtenir l’entrée courante du tableau ou de savoir si le tableau contient une autre entrée pour continuer son parcours par exemple. Il sera capable de le gérer tout seul.
Pour créer un générateur, il suffit de créer une fonction qui à la place de retourner un tableau, utilisera autant de fois que nécessaire le mot-clé yield
pour indiquer à la foncction générateur, les éléments à parcourir au moment où vous bouclerez dessus :
<?php
public function read(string $filename): \Generator
{
$handle = fopen($filename, "r");
// $rows = [];
while(!feof($handle)) {
// $rows[] = fgets($handle);
yield fgets($handle);
}
fclose($handle);
// return $rows;
}
Comme vous pouvez le voir, on parcourt toujours les lignes du fichier mais plutôt que de renvoyer le tableau avec toutes les lignes, on utilise le mot-clé yield
pour indiquer à PHP les lignes du fichier que le générateur devra parcourir.
Lorsque la fonction read
est appelée, elle retourne un objet Generator
que l'on peut parcourir avec foreach
:
$rows = $this->read('input.txt');
foreach($rows as $row) {
// traiter les données
}
Lorsque vous parcourez cet objet, PHP appellera les méthodes d'itération de l'objet (rewind()
, valid()
, key()
, current()
) chaque fois qu'il a besoin d'une valeur. Ensuite, il sauvegardera le statut du générateur en appelant la méthode next()
de l'itérateur au moment où il génère une valeur. Lorsqu'il n'y a plus de valeur à fournir (valid()
retourne false), la fonction générateur peut simplement sortir, et le code appelant continuera comme si un tableau n'avait plus de valeur.
Le générateur consommera autant de mémoire qu'un itérateur, c'est à dire très peu.
Problème
À la différence des itérateurs, le générateur est capable d'aller uniquement vers l'avant. Il ne peut donc pas être ré-initialisé une fois que le parcours a commencé. Cela signifie que le même générateur ne peut pas être utilisé à plusieurs reprises : il devra être reconstruit en appelant une nouvelle fois la fonction générateur.
$rows = $this->read('input.txt');
foreach($rows as $row) {
// traiter les données
}
foreach($rows as $row) {
// vous ne passerez jamais ici car le générateur a déjà été consommé.
}
Conclusion
La lecture d'un fichier ligne par ligne avec un tableau PHP est à bannir si vous travailler avec des gros fichiers.
Le générateur ne vous permettra pas de lire plusieurs fois le fichier, vous devrez appeler une nouvelle fois la fonction générateur à chaque fois que vous voulez revenir au début du fichier. Si vous devez parcourir une seule fois le fichier, je vous recommande d'utiliser les générateurs qui sont plus simple à créer par rapport à des itérateurs.
L'itérateur est la solution à retenir si vous avez besoin de traiter plusieurs fois les données du fichier. Vous serez capable de revenir au début du fichier à n'importe quel moment.
Ressources
- Lire un fichier dans un tableau avec la fonction
file()
oufgets()
- Générateur PHP
- Interface Iterator
- Comparaison des générateurs avec les objets Itérateurs
- Patron de conception Itérateur
Credits
Image de Jordi Koalitic
Top comments (0)