Mis à jour le lundi 8 janvier 2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les générateurs

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Les générateurs sont une façon simple et rapide d’implémenter des itérateurs (terme que nous avons vu au cours du chapitre sur les interfaces), permettant ainsi de résoudre des problèmes de performance ou de code à rallonge. Nous allons commencer doucement en découvrant les bases de cet outil, pour ensuite l’explorer plus profondément et découvrir toute sa puissance.

Afin de suivre au mieux ce chapitre, il est obligatoire d’avoir bien compris le chapitre portant sur les interfaces, et plus particulièrement d’être familier avec l’interfaceIterator (relisez la définition d’un itérateur si vous l’avez oubliée).

Notions de base

Étude de cas

Définissons ce qu’est un générateur par une étude de cas. Imaginons que vous vouliez parcourir les lignes d’un fichier pour faire une quelconque opération sur chacune d’entre elles. Pour ce faire, vous avez la fonction file qui a pour rôle de lire le fichier puis de retourner un tableau dont chaque entrée est une ligne différente. Son utilisation est ainsi plutôt simple :

<?php
$lines = file('MonFichier');

foreach ($lines as $line)
{
  // Effectuer une opération sur $line
}

Comme vous l’avez peut-être remarqué, cela devient vite embêtant si le fichier est gros. Imaginez qu’il fasse des milliers de lignes, chacune comportant des centaines de caractères, ça ne vous embêterait pas de stocker tout ça dans une variable ? Il y a de gros risques pour atteindre la limite de la mémoire allouée pour le script.

Ce qu’il faudrait donc faire, ce serait lire les lignes une par une, sans garder en mémoire la valeur de la précédente ligne. Si l’on veut garder la boucleforeach pour conserver cet aspect pratique d’utilisation, nous allons donc devoir utiliser un itérateur (on vient de voir que le tableau déjà rempli n’est pas la solution, il ne nous reste donc plus beaucoup d’options !). Voilà à quoi pourrait ressembler notre classe :

<?php
class FileReader implements Iterator
{
  protected $file;
 
  protected $currentLine;
  protected $currentKey;
 
  public function __construct($file)
  {
    if (!$this->file = fopen($file, 'r'))
    {
      throw new RuntimeException('Impossible d\’ouvrir "' . $file . '"');
    }
  }
 
  // Revient à la première ligne
  public function rewind()
  {
    fseek($this->file, 0);
    $this->currentLine = fgets($this->file);
    $this->currentKey = 0;
  }
 
  // Vérifie que la ligne actuelle existe bien
  public function valid()
  {
    return $this->currentLine !== false;
  }
 
  // Retourne la ligne actuelle
  public function current()
  {
    return $this->currentLine;
  }
 
  // Retourne la clé actuelle
  public function key()
  {
    return $this->currentKey;
  }
 
  // Déplace le curseur sur la ligne suivante
  public function next()
  {
    if ($this->currentLine !== false)
    {
      $this->currentLine = fgets($this->file);
      $this->currentKey++;
    }
  }
}

L’utilisation de cet itérateur est aussi simple qu’avec la fonctionfile :

<?php
$fileReader = new FileReader('MonFichier');

foreach ($fileReader as $line)
{
  // Effectuer une opération sur $line
}

Comme vous le voyez, bien qu’on ait fait une grande optimisation au niveau de la mémoire utilisée, nous avons pourtant un code bien plus long : la création d’itérateur telle que vous la connaissez est longue, surtout pour ne faire qu’une petite opération comme c’est le cas ici. C’est exactement à cela que remédient les générateurs : ils optimisent l’utilisation de la mémoire tout en conservant un code clair et concis.

Les générateurs

Entrons maintenant dans le vif du sujet. Un générateur, comme brièvement expliqué, permet la création d’itérateur de manière simple et efficace. Regardons de nouveau l’exemple de la classe précédemment créée. Si nous voulions résoudre ce problème de longueur, que ferions-nous ? La première idée serait de ne pas avoir à écrire de classe. En effet, si on s’attarde un peu sur le contenu des méthodes, seule la méthodenext est vraiment spécifique à notre cas : c’est dans cette méthode qu’on construit le tableau à parcourir. Les autres méthodes (rewind ,valid ,current  etkey ) ne sont pas spécifiques à ce qu’on fait. En effet, elles permettent juste de traiter le tableau qu’on a construit, mais on ne fait rien d’exceptionnel : il s’agit d’un tableau classique que PHP peut très bien parcourir tout seul.

C’est de cette idée que sont nés les générateurs : n’écrire qu’une fonction qui est chargée de construire le tableau, sans se soucier de toutes les autres fonctions 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.

Pour créer un générateur, nous n’allons ainsi écrire qu’une seule fonction. Dans cette fonction, on va parcourir les lignes du fichier et, pour chaque ligne, on va indiquer à PHP qu’il s’agit de la valeur de la prochaine entrée du tableau grâce au mot-cléyield  (retenez-le bien, vous allez le voir pas mal de fois dans la suite de ce chapitre !). On va donc « construire » petit à petit notre tableau en lui ajoutant des entrées au fur et à mesure (nous verrons plus tard et plus en détails comment cela fonctionne).

<?php
function readLines($fileName)
{
  // Si le fichier est inexistant, on ne continue pas
  if (!$file = fopen($fileName, 'r'))
  {
    return;
  }
 
  // Tant qu'il reste des lignes à parcourir
  while (($line = fgets($file)) !== false)
  {
    // On dit à PHP que cette ligne du fichier fait office de « prochaine entrée du tableau »
    yield $line;
  }
 
  fclose($file);
}

Cette fonction est un peu particulière (si vous ne la comprenez pas tout de suite - et ce serait normal -, continuez, vous comprendrez). Rappelez-vous ce que je vous ai dit plus haut : le but des générateurs est de créer facilement des itérateurs. Un générateur est un itérateur. Et qu’est-ce qu’un itérateur ? Un itérateur est une instance d’une classe implémentantIterator . Donc… oui, un générateur est une instance d’une classe implémentantIterator ! Or notre fonction est un générateur car elle contient le mot-cléyield dedans (c’est automatique : toute fonction contenant ce mot-clé est considérée comme un générateur par PHP).

Vous ne comprenez peut-être pas où je veux en venir. Ce que je veux dire, c’est que cette fonction n’en est une qu’à première vue, mais ce que vous venez d’écrire est en fait une sorte de classe : cette fonction que vous avez écrite est « transformée » par PHP en une instance de la classeGenerator. Besoin d’une preuve ? Essayez ceci :

<?php
var_dump(readLines('MonFichier'));

Me croyez-vous maintenant ? Votre fonction n’en est ainsi pas vraiment une, il ne s’agit ni plus ni moins d’une fonction générateur qui, lorsque vous l’appelez, retourne une instance deGenerator, instance que vous pourrez ainsi parcourir. Cette classe implémente l’interfaceIterator et gère de base les méthodes s’occupant du parcours du « tableau ».

Si vous avez effectué le test précédent, vous avez pu voir que le fait d’avoir appelé notre fonctionreadLines n’a en rien lancé l’exécution de celle-ci. Il est impossible de l’invoquer : si vous l’appelez comme on l’a fait, vous obtiendrez juste l’instance deGenerator associée à ce générateur.

Maintenant qu’on a un itérateur, nous n’avons qu’à le parcourir !

<?php
$generator = readLines('MonFichier');

foreach ($generator as $line)
{
  // Effectuer une opération sur $line
}

Je crois percevoir en vous une légère perplexité. Plus précisément, il est possible que vous ne compreniez pas, étape par étape, ce qu’il se passe. Si je détaille le dernier script, voici ce que ça donne.

On commence par récupérer l’instance deGenerator associée au générateur. La variable$generator est donc un itérateur. La fonction n’a pas encore été exécutée. Vient ensuite le parcours de l'itérateur grâce à la boucleforeach :

  1. Première itération : la fonction commence à s’exécuter. PHP continue l’exécution de la fonction jusqu’à ce qu’il rencontre unyield .

  2. PHP rencontre le yield suivi d’une chaine de caractères (il s’agit de la ligne actuelle du fichier). Il va donc dire à la boucle foreach  : « tiens, la prochaine valeur est cette chaine de caractères ».

  3. PHP arrête l’exécution de la fonction (il ne va pas plus loin que leyield qu’il a rencontré).

  4. La boucleforeach peut donc commencer, et la valeur courante du tableau (ici représentée par la variable$line) n’est autre que la valeur spécifiée avec leyield dans la fonction.

  5. Une fois l’itération de la boucleforeach terminée, on recommence : PHP va continuer l’exécution de la fonction là où il s’était arrêté, puis s’arrêtera de nouveau lorsqu’il rencontrera unyield. On retourne ainsi à l’étape 2, et ainsi de suite jusqu’à ce que la fonction se termine.

Besoin d’un schéma pour mieux visualiser ce qu’il se passe ?

Fonctionnement du parcours d'un générateur
Fonctionnement du parcours d'un générateur

Bien sûr, puisquereadLines('MonFichier')  renvoie un identifiant d’objet, nous n’avons pas besoin de passer par une variable : nous pouvons directement parcourir ce résultat avecforeach.

<?php
foreach (readLines('MonFichier') as $line)
{
  // Effectuer une opération sur $line
}

De cette façon, on a réglé le problème de la mémoire saturée (PHP ne garde en mémoire qu’une ligne à la fois ; quand il passe à la ligne suivante, il a oublié la précédente), ainsi que le problème du code à rallonge. Voici donc une première introduction aux générateurs !

Zoom sur les valeurs retournées

Retourner des clés avec les valeurs

Il y a différentes façons de retourner des valeurs avecyield. Dans l’exemple précédent, nous n’avions retourné qu’une simple valeur (il s’agissait d’une chaîne de caractères, mais vous pouvez retourner n’importe quoi). Or, comme vous le savez, dans une boucleforeach, il est possible de récupérer la clé associée à l’entrée actuellement parcourue du tableau. Par défaut, lorsque vous faites unyield, PHP va incrémenter son compteur de sorte à fournir des clés numériques, comme pour les tableaux (la première valeur est disponible à la clé 0, la deuxième à la clé 1, etc.). Essayez par vous-mêmes :

<?php
function generator()
{
  for ($i = 0; $i < 10; $i++)
  {
    yield 'Itération n°'.$i;
  }
}

foreach (generator() as $key => $val)
{
  echo $key, ' => ', $val, '<br />';
}

Ce qui vous affichera :

Résultat affiché par le script
Résultat affiché par le script

Sachez qu’il est possible de modifier la clé associée à la valeur que vous retournez. Pour cela, vous devez suivre cette syntaxe :

<?php
yield $key => $val;

Voici un exemple simple mettant en application cette nouvelle syntaxe.

<?php
function generator()
{
  // On retourne ici des chaines de caractères assignées à des clés
  yield 'a' => 'Itération 1';
  yield 'b' => 'Itération 2';
  yield 'c' => 'Itération 3';
  yield 'd' => 'Itération 4';
}

foreach (generator() as $key => $val)
{
  echo $key, ' => ', $val, '<br />';
}

Ce qui vous affichera le résultat suivant.

Résultat affiché par le script
Résultat affiché par le script

Nous venons ainsi de voir une deuxième utilisation deyield. Il ne nous restera ainsi qu’une troisième façon que nous verrons dans la prochaine partie.

Retourner une référence

Imaginez que vous ayez une classe contenant un tableau. Vous voulez pouvoir parcourir ce tableau depuis l’extérieur de la classe, tout en pouvant modifier ses valeurs. Pour ce faire, il faudra passer ces valeurs par référence. Commençons avec une classe simple qui donne la possibilité de parcourir le tableau qu’elle a en attribut (sans parler de référence).

<?php
class SomeClass
{
  protected $attr;

  public function __construct()
  {
    $this->attr = ['Un', 'Deux', 'Trois', 'Quatre'];
  }

  public function generator()
  {
    foreach ($this->attr as $val)
    {
      yield $val;
    }
  }
}

Je vous laisse le soin d’écrire la boucleforeach sur notre générateur. Un petit coup de pouce si vous êtes coincé :

<?php
$obj = new SomeClass;

foreach ($obj->generator() as $val)
{
  var_dump($val);
}

Le but de la manoeuvre est de faire en sorte que l’on puisse modifier$val  dans notre boucleforeach. Pour ce faire, nous allons dire à notre fonction qu’elle doit renvoyer des références. Comme vous le savez, pour qu’une fonction renvoie une référence, on doit placer un & devant son nom (si vous ne le saviez pas, allez relire ce tutoriel). Pour les générateurs, on fait pareil : lorsqu’on ajoute un & devant son nom, cela voudra dire que toutes les variables retournées paryield seront retournées par référence. C’est cette étape la plus importante, et c’est celle-là qu’il faut retenir.

Si l’on revient à notre exemple, cela ne suffira pas. En effet, dans la boucleforeach de notre générateur, il faut récupérer les variables du tableau par référence aussi. Bien sûr, il en va de même pour la boucleforeach parcourant notre générateur. On obtiendrait donc un code ressemblant à celui-ci.

<?php
class SomeClass
{
  protected $attr;

  public function __construct()
  {
    $this->attr = ['Un', 'Deux', 'Trois', 'Quatre'];
  }

  // Le & avant le nom du générateur indique que les valeurs retournées sont des références
  public function &generator()
  {
    // On cherche ici à obtenir les références des valeurs du tableau pour les retourner
    foreach ($this->attr as &$val)
    {
      yield $val;
    }
  }

  public function attr()
  {
    return $this->attr;
  }
}

$obj = new SomeClass;

// On parcourt notre générateur en récupérant les entrées par référence
foreach ($obj->generator() as &$val)
{
  // On effectue une opération quelconque sur notre valeur
  $val = strrev($val);
}

echo '<pre>';
var_dump($obj->attr());
echo '</pre>';

Le résultat affiché devrait être celui-ci.

Résultat affiché par le script
Résultat affiché par le script

Ce qui prouve bien que le tableau a été modifié en dehors de la classe, mettant ainsi en avant l’utilisation des références.

Les coroutines

Si vous jetez un œil à la classe Generator qui est, je vous le rappelle, la classe dont chaque générateur est une instance, vous pouvez vous apercevoir qu’elle dispose de 3 méthodes de plus que Iterator :send,throw  et__wakeup. Nous allons ici étudier les deux premières qui méritent notre attention.

La méthode « send »

Commençons par cette méthode très intéressante (et sans doute un peu difficile à cerner au début).

Imaginons un système « inversé ». Actuellement, le générateur fournit des données. Et bien sachez qu’il est possible de faire l’inverse, c’est-à-dire envoyer des données au générateur (nous verrons un intérêt plus tard).

Pour ce faire, je vais vous montrer un exemple tout simple.

<?php
function generator()
{
  echo yield;
}

$gen = generator();
$gen->send('Hello world !');

Si vous testez ce code, vous verrez que la phrase Hello world ! s’affichera sur votre écran. Alors, que s’est-il passé ?

Nous commençons par simplement récupérer le générateur dans notre variable$gen . Ensuite, nous invoquons la méthodesend  en lui envoyant Hello world ! en argument. Lorsque vous invoquezsend pour la première fois, PHP va commencer l’exécution de la fonction jusqu’au prochainyield qu’il rencontre. Lorsqu’il en rencontre un (peu importe si vous avez spécifié une valeur à retourner ou non), PHP « remplacera » ceyield par la valeur spécifiée dans la méthodesend. Une fois fait, la fonction continue son exécution jusqu’au prochainyield, puis PHP la met en pause juste avant le prochainyield. S’il ne s’agit pas du premier appel à la méthodesend, PHP reprendra l’exécution de la fonction là où il s’était arrêté, puis refera la même opération que précédemment, etc.

Avant d’aller plus loin, je voudrais faire une petite parenthèse sur la syntaxe à respecter. En effet, vous êtes maintenant au courant qu’il y a 2 cas d’utilisation du mot-cléyield : soit il est utilisé dans une expression, c’est-à-dire qu’on s’intéresse au résultat qu’il retournera (comme on vient juste de le faire avec leecho yield ), soit il est utilisé seul et constitue à lui-même une instruction (comme on faisait avant, c’est-à-dire en faisant unyield $data; par exemple).

Dans le cas où leyield est utilisé dans le contexte d’une expression, des parenthèses sont requises autour de lui, sauf siyield est utilisé seul. Dans l’autre cas, les parenthèses ne sont pas nécessaires.

Voici un petit exemple mettant en oeuvre les 3 cas différents à distinguer :

<?php
// Le yield n'est pas utilisé dans une expression : pas de parenthèses
yield 'Hello world !';

// Le yield est ici utilisé dans une expression, mais il est utilisé seul : pas de parenthèses
$data = yield;

// Le yield est ici utilisé dans une expression : les parenthèses sont requises
$data = (yield 'Hello world !');

N’hésitez pas à faire plusieurs tests pour vous faire la main ! En voici déjà un qui met en avant quelques situations.

<?php
function generator()
{
  echo (yield 'Hello world !');
  echo yield;
}

$gen = generator();

 // On envoie « Message 1 »
// PHP va donc l'afficher grâce au premier echo du générateur
$gen->send('Message 1');

// On envoie « Message 2 »
// PHP reprend l'exécution du générateur et affiche le message grâce au 2ème echo
$gen->send('Message 2');

// On envoie « Message 3 »
// La fonction générateur s’était déjà terminée, donc rien ne se passe
$gen->send('Message 3');

Lorsque l’on utilise les générateurs de cette façon (c’est-à-dire qu’on les utilise pour prendre des valeurs et non en retourner), on parle de générateur inverse ou encore de coroutine.

Voyons maintenant un exemple d’utilisation. Je préfère vous prévenir à l’avance : il s’agit de quelque chose de plutôt difficile à cerner (surtout si vous avez commencé ce chapitre dans les 20 dernières minutes). Il est donc normal que vous ne compreniez pas tout ce qui suit du premier coup : si vous bloquez, passez à la suite, puis revenez-y dans quelques jours !

Nous allons ici voir comment on pourrait faire un système multitâche. Concrètement, on va faire en sorte de pouvoir exécuter des fonctions « en parallèle », c’est-à-dire des fonctions qui se mettent en pause chacun leur tour afin qu’une autre puise poursuivre son exécution.

On va donc créer une classe dont le rôle sera de gérer ces tâches, c’est-à-dire qu’elle possédera une liste de tâches et sera capable de les exécuter en parallèle. Pour des raisons pratiques, cette liste sera sous la forme d’une instance de SplQueue. Cette classe nous permettra de gérer facilement notre liste de tâches grâce aux méthodes enqueue, dequeue et isEmpty, permettant respectivement d’ajouter un élément en fin de liste, de supprimer le premier élément de la liste et de savoir si la liste est vide.

Commençons par écrire la base de notre classeTaskRunner dont on vient de parler.

<?php
class TaskRunner
{
  protected $tasks;

  public function __construct()
  {
    // On initialise la liste des tâches
    $this->tasks = new SplQueue;
  }

  public function addTask(Generator $task)
  {
    // On ajoute la tâche à la fin de la liste
    $this->tasks->enqueue($task);
  }
  
  public function run()
  {
    // On verra ici ce qu’on mettra
  }
}

 

Vous devez sans doute vous demander comment il peut être possible d’exécuter deux fonctions (ou plus) en parallèle. L’astuce ici se fera bien entendu à l’aide deyield dans les fonctions représentant les tâches, afin de les mettre en pause régulièrement. Le principe sera donc le suivant.

On a une liste de tâches qu’on parcourt tant qu’elle n’est pas vide (on a la méthode isEmpty à notre disposition). À chaque nouvelle tâche, on va invoquersend sur cette tâche en lui envoyant les données dont elle a besoin (dans notre cas, on va se contenter d’envoyer « Hello world ! » pour garder quelque chose de simple). Ensuite, il faut enlever cette tâche du haut de la liste grâce à dequeue (cette méthode renvoie la valeur de l’élément supprimé, donc on va d’abord l’appeler afin de récupérer la tâche actuelle puis ensuite appelersend). Enfin, on va voir si la tâche est finie. Si elle n’est pas finie, il nous suffit de rajouter la tâche à la fin de la liste grâce à enqueue. Pour cela, n’oubliez pas ce que sont nos tâches : ce sont des générateurs, donc des itérateurs. Par conséquent, vous avez à votre disposition la méthodevalid permettant de vérifier s’il y a une prochaine valeur (dans notre cas, cela revient à vérifier s’il y a un prochainyield, donc de vérifier si la tâche a encore quelque chose à faire).

On est donc dans la capacité d’écrire notre méthoderun :

<?php
class TaskRunner
{
  protected $tasks;

  public function __construct()
  {
    // On initialise la liste des tâches
    $this->tasks = new SplQueue;
  }

  public function addTask(Generator $task)
  {
    // On ajoute la tâche à la fin de la liste
    $this->tasks->enqueue($task);
  }

  public function run()
  {
    // Tant qu’il y a toujours au moins une tâche à exécuter
    while (!$this->tasks->isEmpty())
    {
      // On enlève la première tâche et on la récupère au passage
      $task = $this->tasks->dequeue();

      // On exécute la prochaine étape de la tâche
      $task->send('Hello world !');

      // Si la tâche n’est pas finie, on la replace en fin de liste
      if ($task->valid())
      {
        $this->addTask($task);
      }
    }
  }
}

Vous pouvez essayer ce code avec des tâches simples comme celles-ci.

<?php
$taskRunner = new TaskRunner;

function task1()
{
  for ($i = 1; $i <= 2; $i++)
  {
    $data = yield;
    echo 'Tâche 1, itération ', $i, ', valeur envoyée : ', $data, '<br />';
  }
}

function task2()
{
  for ($i = 1; $i <= 6; $i++)
  {
    $data = yield;
    echo 'Tâche 2, itération ', $i, ', valeur envoyée : ', $data, '<br />';
  }
}

function task3()
{
  for ($i = 1; $i <= 4; $i++)
  {
    $data = yield;
    echo 'Tâche 3, itération ', $i, ', valeur envoyée : ', $data, '<br />';
  }
}

$taskRunner->addTask(task1());
$taskRunner->addTask(task2());
$taskRunner->addTask(task3());

$taskRunner->run();

Vous devriez donc voir que chaque tâche s’est exécuté en parallèle en analysant le résultat affiché :

Résultat affiché par le script
Résultat affiché par le script

Voilà donc cet exemple terminé. N’hésitez pas à le relire pour bien cerner le principe ! Une fois que vous avez bien compris ce qui se passait, je vous encourage à vous appuyer sur cet exemple pour faire un système plus poussé (envoyer des données spécifiques aux tâches, définir un ordre de priorité, etc.)

La méthode « throw »

La méthodethrow permet de lancer une exception à l’emplacement duyield dans le générateur. L’idée est la même que poursend : lorsquethrow  est appelée, PHP démarre (ou continue) l’exécution du générateur jusqu’au prochainyield, et lancera une exception à cet endroit précis. Cette méthode accepte un seul et unique argument : l’exception à lancer (donc une instance deException  ou une instance d'une classe héritant deException ).

Voyez vous-mêmes par cet exemple :

<?php
function generator()
{
  echo "Début\n";
  yield;
  echo "Fin";
}

$gen = generator();
$gen->throw(new Exception('Test'));

À votre écran doit donc s’afficher quelque chose comme ça.

Résultat affiché par le script
Résultat affiché par le script

Cela montre que notre fonction s’est bien exécutée jusqu’au premieryield, et dès que PHP y est arrivé, il a lancé l’exception. Pour l’attraper, vous savez déjà faire ! Ici, aux yeux de PHP, c’est leyield qui va causer le lancement de l’exception. Par conséquent, c’est leyield qui va devoir être entouré du bloctry.

Pour vous familiariser un peu avec ce concept, voici un petit exemple qui devrait vous aider à comprendre.

<?php
function generator()
{
  // On fait une boucle de 5 yield pour garder quelque chose de simple
  for ($i = 0; $i < 5; ++$i)
  {
    // On indique qu’on vient de rentrer dans la ième itération
    echo "Début $i<br />";
    
    // On essaye « d’attraper » la valeur qu’on nous a donnée
    try
    {
      yield;
    }
    catch (Exception $e)
    {
      // Si une exception a été levée, on indique son numéro
      echo "Exception $i<br />";
    }
    
    // Enfin, on indique qu’on vient de finir la ième itération
    echo "Fin $i<br />";
  }
}

$gen = generator();

foreach ($gen as $i => $val)
{
  // On décide de lancer une exception pour l’itération n°3
  if ($i == 3)
  {
    $gen->throw(new Exception('Petit test'));
  }
}

Vous devriez obtenir un résultat comme celui-ci.

Résultat affiché par le script
Résultat affiché par le script

Vous voyez donc qu’un seulyield (celui de la 3ème itération) a mené à une exception, qui d’ailleurs a été attrapée sans souci avec les fameux bloctry/catch.

Comme vous pouvez vous en douter, lancer une exception sans aucune raison comme on vient de le faire a très peu d’intérêt. En fait, cette méthodethrow va de paire avecsend. Lorsque vous avez un système de coroutine comme on vient de le voir, c’est à vous de fournir les données au générateur. Si quelque chose ne va pas, vous pouvez lancer une exception pour le prochainyield au lieu de lui envoyer des données avecsend.

Pour reprendre notre système multitâche précédent, on pourrait ajouter la possibilité de tuer une tâche en cours d’exécution. Pour ce faire, il nous suffirait d’envoyer une exception à notre tâche pour lui indiquer qu’il faut qu’elle se termine.

Voici le code que je vous propose (je décide de tuer la tâche n°2 lors de la deuxième itération de manière totalement arbitraire, c’est juste pour l’exemple).

<?php
class TaskRunner
{
  protected $tasks;

  public function __construct()
  {
    // On initialise la liste des tâches
    $this->tasks = new SplQueue;
  }

  public function addTask(Generator $task)
  {
    // On ajoute la tâche à la fin de la liste
    $this->tasks->enqueue($task);
  }

  public function run()
  {
    $i = 1;

    // Tant qu’il y a toujours au moins une tâche à exécuter
    while (!$this->tasks->isEmpty())
    {
      // On enlève la première tâche et on la récupère au passage
      $task = $this->tasks->dequeue();

      // Pour l'exemple, on va arrêter la tâche n°2 lors de son 2ème appel
      if ($i == 5)
      {
        $task->throw(new Exception('Tâche interrompue'));
      }

      // On exécute la prochaine étape de la tâche
      $task->send('Hello world !');

      // Si la tâche n’est pas finie, on la replace en fin de liste
      if ($task->valid())
      {
        $this->addTask($task);
      }

      $i++;
    }
  }
}

$taskRunner = new TaskRunner;

function task1()
{
  for ($i = 1; $i <= 2; $i++)
  {
    try {
      $data = yield;
      echo 'Tâche 1, itération ', $i, ', valeur envoyée : ', $data, '<br />';
    } catch(Exception $e) {
      echo 'Erreur tâche 1 : ', $e->getMessage(), '<br />';
      return;
    }
  }
}

function task2()
{
  for ($i = 1; $i <= 6; $i++)
  {
    try {
      $data = yield;
      echo 'Tâche 2, itération ', $i, ', valeur envoyée : ', $data, '<br />';
    } catch(Exception $e) {
      echo 'Erreur tâche 2 : ', $e->getMessage(), '<br />';
      return;
    }
  }
}

function task3()
{
  for ($i = 1; $i <= 4; $i++)
  {
    try {
      $data = yield;
      echo 'Tâche 3, itération ', $i, ', valeur envoyée : ', $data, '<br />';
    } catch(Exception $e) {
      echo 'Erreur tâche 3 : ', $e->getMessage(), '<br />';
      return;
    }
  }
}

$taskRunner->addTask(task1());
$taskRunner->addTask(task2());
$taskRunner->addTask(task3());

$taskRunner->run();

En exécutant ce code, vous devriez vous apercevoir que la tâche n°2 a bien été arrêtée :

Résultat affiché par le script
Résultat affiché par le script

En résumé

  • Les générateurs sont une façon simple de créer des itérateurs.

  • Toute fonction contenant le mot-cléyield est automatiquement considéré comme un générateur.

  • Un générateur peut renvoyer une valeur simple mais aussi une clé qui lui sera associée.

  • Pour renvoyer une référence via unyield, il faut placer un & avant le nom du générateur.

  • La méthodesend permet de créer des coroutines, ce qui consiste à consommer des valeurs et non à en retourner.

Exemple de certificat de réussite
Exemple de certificat de réussite