Présentation des générateurs

Pascal MARTIN m’a souvent dit « les générateurs c’est fantastique ». J’ai donc voulu essayer.

Les générateurs sont apparus en 2013 avec PHP 5.5. Le concept et son utilisation peuvent paraître un peu abstrait. Je vais donc essayer de vous donner une idée d’utilisations avec les générateurs.

Je vais vous présenter une utilisation que j’ai pu trouver de conversion « à la volée » pendant un export. J’ai utilisé cette solution dans un projet réel mais cet article ne sera qu’une illustration simple.

Présentation

J’ai créé un petit projet pour illustrer cet article : GeneratorExample. Ce projet permet l’export en CSV d’une liste de Person. Pour cet export nous utilisons la méthode fputcsv. Nous avons donc besoin de convertir les objets Person en tableau.

Voici la classe Person :

<?php

class Person {

    /**
    * @var string
    */
    private $firstName;

    /**
    * @var string
    */
    private $lastName;

    /**
    * @var \DateTime
    */
    private $birthday;

    public function __construct($lastName, $firstName, \DateTime $birthday)
    {
        $this->lastName = $lastName;
        $this->firstName = $firstName;
        $this->birthday = $birthday;
    }

    /**
    * @return string
    */
    public function getFirstName()
    {
        return $this->firstName;
    }

    /**
    * @return string
    */
    public function getLastName()
    {
        return $this->lastName;
    }

    /**
    * @return \DateTime
    */
    public function getBirthday()
    {
        return $this->birthday;
    }

}

Et la classe PersonRepository :

<?php

class PersonRepository
{

    private $persons;

    public function __construct(array $persons)
    {
        $this->persons = $persons;
    }

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

    public function findAllAsArray()
    {
        // Use $this->findAll() and converPersonAsArray()
    }

    protected function convertPersonAsArray(Person $person)
    {
        return [
            $person->getLastName(),
            $person->getFirstName(),
            $person->getBirthday()
        ];
    }

}

Cette classe est une version très allégée (pour l’exemple) d’un Repository Doctrine avec la méthode findAll() qui nous retourne toutes les personnes. Dans une version réelle les données viendraient généralement d’une base de données.

Pour la conversion de la classe Person en tableau, j’ai créé une methode findAllAsArray(). Cette méthode à pour but de prendre les personnes retournées par findAll() et de convertir chaque objet Person en un tableau.

Nous allons voir dans un premier temps l’utilisation d’un tableau pour stocker les conversions des personnes. Ensuite nous verrons la conversion avec l’utilisation d’un générateur.

Une autre solution aurait été d’utiliser un Iterator mais le générateur est Iterator avec une écriture très allégée donc nous ne la présenterons pas.

Utilisation des tableaux

Nous avons vu que nous pouvons effectuer notre conversion en stockant les résultats dans un tableau à la manière suivante :

public function findAllAsArray()
{
    $persons = [];

    foreach ($this->findAll() as $person) {
        $persons[] = $this->convertPersonAsArray($person);
    }

    return $persons;
}

Cette méthode est très simple et très lisible. Le problème de cette solution est que l’on utilise beaucoup de mémoire si la fonction findAll() retourne beaucoup de personnes. Dans le cas où findAll() utilise un Traversable nous perdons le chargement « au besoin ».

C’est là que les générateurs vont être utiles.

Utilisation des générateurs

Nous pouvons donc utiliser un générateur comme dans la méthode suivante :

    public function findAllAsArray()
    {
        $persons = $this->findAll();

        if (empty($persons)) {
            return;
        }

        foreach ($persons as $person) {
            yield $this->convertPersonAsArray($person);
        }
    }

Le code reste très lisible et nous gagnons en preformance.

Bench

J’ai créé le script command.php pour pouvoir réaliser des tests comparatifs entre les deux méthodes. Si on utilise l’argument array on utilise les conversions stockées dans un tableau, dans les autres cas nous utilisons la conversion via le générateur. J’ai utilisé une table de données un peu importante pour avoir du contenu et permettre une meilleure comparaison.

$ php command.php
Number of persons : 90 112
Memory peak after data loading : 13 850 736 B
Memory peak after export : 18 512 808 B
Memory used by export : 4 662 072 B
Time : 0.57722878456116 ms

$ php command.php array
Number of persons : 90 112
Memory peak after data loading : 13 851 152 B
Memory peak after export : 83 591 608 B
Memory used by export : 69 740 456 B
Time : 0.61213397979736 ms

Nous pouvons voir qu’avec l’utilisation des générateurs nous avons un gain d’au moins 90% de mémoire, dans ce cas, par rapport à l’utilisation des tableaux. Ce gain dépend, bien sur, du volume de données à traiter.

Cerise sur le gateau nous pouvons également voir que le temps d’éxécution est (un peu) plus court avec les générateurs.

Conclusion

Nous avons pu voir une utilisation des générateurs pour convertir des données. La solution n’est pas plus compliquée à écrire que le fait d’utiliser un tableau.

Nous aurions pu jouer la conversion des données au moment où nous utilisions la fonction fputcsv le problème est que nous perdons le principe de ‘Responsabilité unique’ (Single responsibility) conseillé dans le développement SOLID.

En parlant de SOLID nous aurions dû utiliser une classe permettant la conversion en dehors du Repository mais cela aurait plus complexifié l’exemple.

Un autre gros avantage des générateurs c’est que nous pouvons les chainer, comme avec le script chained_generators dont voici le résultat :

$ php chained_generators.php
Array
(
    [id] => 1
    [first_name] => Harry
    [last_name] => POTTER
    [birthday] => 1980-08-31
)
Array
(
    [id] => 2
    [first_name] => Ron
    [last_name] => WEASLEY
    [birthday] => 1980-03-01
)
Array
(
    [id] => 3
    [first_name] => Hermione
    [last_name] => GRANGER
    [birthday] => 1979-09-19
)

Nous avons donc des conversions successives qui se font en consommant une quantité réduite de ressources.

Merci de votre lecture.

Merci à Pascal MARTIN sans qui cet article n’aurait jamais vu le jour, ainsi que pour sa relecture.

2 réflexions sur « Présentation des générateurs »

  1. Gabriel

    Je connais pas bien les générateurs mais ça m’intrigue ton return :

    « `
    if (empty($persons)) {
    return;
    }
    « `

    Quel est l’effet si il n’y a eu aucun yield ?

    1. florian Auteur de l’article

      Bah écoute, tu m’as mis le doute donc j’ai refais un test et il se passe rien de mal.
      C’est comme si tu rentrais dans un foreach mais avec un tableau vide.

Les commentaires sont fermés.