Comment implémenter l’interface Iterator de PHP

Exemple d’implémentation de l’interface native de PHP Iterator.

Article écrit par Web and Cow. N'hésite pas découvrir son activité, ses valeurs et ses offres proposées.

L’interface native Iterator de PHP va permettre de rendre un objet « itérable », c’est à dire que l’on va pouvoir le parcourir avec un foreach, comme un tableau.

Cas d’usage et conception

Pour un client, j’ai développé il y a quelques années un système permettant à des éleveurs laitiers de suivre leurs analyses de lait. Ces éleveurs ont un compte sur l’outil, auquel ils peuvent se connecter avec une adresse mail. Ils peuvent aussi renseigner des mails de contact complémentaires. Enfin, ces éleveurs sont suivis par un ou plusieurs techniciens, également identifiés par leur adresse mail. Quand une nouvelle analyse arrive, il faut prévenir tout ce petit monde par mail. Je dois donc à la fois récupérer les mails, contrôler leur format, vérifier leur unicité, puis envoyer les mails à chacun.

Comment est ce que je peux gérer ça ?

Souvent, face à un problème, je commence par la fin, je me demande « comment j’ai envie de gérer tous ces cas de figure ? ». Je passe dans mon code, par différents endroits, à la recherche d’une ou plusieurs adresses mails. Je ne vais pas tout recontrôler à chaque fois, etc. Ce que j’ai envie de faire, c’est ajouter soit 1 mail, soit plusieurs mails. Pouvoir faire quelque chose comme ça :

// Je crée une classe
$uniqueEmails = new UniqueEmails;

// J'ajoute un mail
$uniqueEmails->addOne($email);

// J'ajoute plusieurs mails, qui sont séparés par exemple par un ";"
$uniqueEmails->addMany($emails, ';');

Et à la fin, je veux pouvoir parcourir ces emails :

foreach ($uniqueEmails as $email) {
   // ... ma logique d'envoi
}

C’est vraiment un process que je conseille. Avant de te lancer dans l’écriture d’une classe ou autre, commence par imaginer comment tu veux l’utiliser. Ca te donnera le plan de développement. Ici :

  1. Je crée ma classe, pas besoin de constructeur
  2. Une méthode addOne
  3. Une méthode addMany
  4. Et implémenter l’interface Iterator pour pouvoir faire le foreach

C’est parti ! Voici la classe et la méthode addOne :

final class UniqueEmails
{
    /**
     * @var string[]
     */
    private array $emails = [];

    public function addOne(string $email): void
    {
        // Au cas où on ait des espaces qui trainent
        $email = trim($email);

        // Contrôle du format
        if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return;
        }

        // Contrôle de l'unicité
        if (in_array($email, $this->emails)) {
            return;
        }

        // Tout est OK, on peut rajouter l'email
        $this->emails[] = $email;
    }
}

Quelques explications :

  • Pas de constructeur, on aurait pu passer un premier email dans le constructeur, mais ce n’est pas obligatoire.
  • La méthode add réalise dans l’ordre ces opérations :
    • Petit nettoyage avec trim s’il y a des espaces autour du mail
    • Contrôle du format à l’aide de la fonction filter_var. Il est possible de renforcer davantage ce contrôle avec des méthodes ou outils + sophistiqués
    • Contrôle de l’unicité grâce à in_array
  • J’utilise ici la notion de « programmation défensive« , c’est à dire que je cherche tout ce qui peut me faire « sortir » de ma fonction au lieu d’imbriquer mes if les uns dans les autres.
  • Si tout est OK, j’ajoute le mail dans ma propriété emails.

Puis la méthode addMany :

public function addMany(string $emails, string $separator = ';'): void
{
    foreach (explode($separator, $emails) as $email) {
        $this->addOne($email);
    }
}

Quelques explications :

  • Il n’est pas nécessaire de récupérer le résultat de explode avant le foreach. Si $emails est vide, alors explode renvoit un tableau vide, donc le foreach ne « tourne » pas.
  • Le séparateur a une valeur par défaut. C’est mieux que de le figer dans le explode. Ca offre de la flexibilité si j’ai plusieurs cas possibles au sein de mon application.

On aurait pu utiliser un array_map pour éviter le foreach, de cette façon :

array_map(
    [$this, 'addOne'],
    explode($separator, $emails)
);

Implémentation de l’interface Iterator de PHP

Une interface, c’est comme un « contrat », c’est à dire que l’on va être obligé de déclarer un ensemble de méthodes prédéfinies. Une interface ne contient que des déclarations de méthodes. Ici le code de l’interface Iterator :

interface Iterator extends Traversable {


    public current(): mixed
    public key(): mixed
    public next(): void
    public rewind(): void
    public valid(): bool
}

L’implémentation de l’interface s’indique dans la déclaration de la classe :

final class UniqueEmails implements \Iterator

Le « \ » est là pour indiquer qu’il s’agit d’une interface native de PHP.

Quand je rajoute juste l’implémentation, mon IDE m’indique l’erreur suivante :

UniqueEmails does not implement methods ‘current’, ‘next’, ‘key’, ‘valid’, ‘rewind’.

Il faut donc créer ces 5 méthodes, dans le contexte de ma classe. Les voici :

final class UniqueEmails implements \Iterator
{
    /**
     * Code précédent, $emails, addOne, addMany
     */

    /**
     * Iterator
     */
    private int $position = 0;

    public function rewind(): void
    {
        $this->position = 0;
    }

    public function key(): int
    {
        return $this->position;
    }

    public function current(): string
    {
        return $this->emails[$this->position] ?? '';
    }

    public function next(): void
    {
        $this->position++;
    }

    public function valid(): bool
    {
        return isset($this->emails[$this->position]);
    }
}

J’ai besoin d’une propriété position. Je la laisse avec les méthodes liées à l’interface Iterator puisqu’elle n’a pas d’utilité ailleurs.

Ces méthodes font appel à la propriété privée emails. Et il est question dans current de retourner une de ses valeurs.

rewind, key, current et next sont aussi des fonctions natives de PHP pour manipuler un tableau et accéder à ses valeurs. Elles présentent ici les mêmes fonctionnalités. N’hésite pas à te référer à la documentation de PHP si tu ne connais pas ces fonctions (entre nous, je ne les utilise que très très rarement).

Le foreach possible grâce à l’interface Iterator

On va donc pouvoir appeler notre objet directement dans un foreach :

foreach ($uniqueEmails as $email) {
    // ... ma logique d'envoi
}

On va donc boucler sur la propriété privée $emails et accéder à ses valeurs !

Conclusion et code complet

PHP propose nativement d’autres interfaces, comme ArrayAcces par exemple, qui permettrait d’aller encore plus loin dans la manipulation « comme un tableau » de la classe. Nous verrons cela dans un prochain article 😉

Et avant de coller le code complet, j’ai lancé une analyse PHPStan de niveau max. J’ai donc rajouté :

  • Une indication @implements dans la documentation de ma classe pour préciser que celle-ci implémente \Iterator et que les index seront des entiers et les valeurs contenues des chaines de caractères.
  • Dans addMany, le séparateur ne peut pas être vide, sinon explode renverra une erreur. Je contrôle donc que $separator n’est pas vide et je lève une exception le cas échéant.
<?php
declare(strict_types=1);


/**
 * @implements \Iterator<int, string>
 */
final class UniqueEmails implements \Iterator
{
    /**
     * @var string[]
     */
    public array $emails = [];

    public function addOne(string $email): void
    {
        // Au cas où on ait des espaces qui trainent
        $email = trim($email);

        // Controle du format
        if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return;
        }

        // Controle de l'unicité
        if (in_array($email, $this->emails)) {
            return;
        }

        // Tout est OK, on peut rajouter l'email
        $this->emails[] = $email;
    }

    public function addMany(string $emails, string $separator = ';'): void
    {
        if ($separator === '') {
            throw new \InvalidArgumentException('Le séparateur ne doit pas être une chaîne de caractères vide.');
        }

        array_map(
            [$this, 'addOne'],
            explode($separator, $emails)
        );
    }

    /**
     * Iterator
     */
    private int $position = 0;

    public function rewind(): void
    {
        $this->position = 0;
    }

    public function key(): int
    {
        return $this->position;
    }

    public function current(): string
    {
        return $this->emails[$this->position] ?? '';
    }

    public function next(): void
    {
        $this->position++;
    }

    public function valid(): bool
    {
        return isset($this->emails[$this->position]);
    }
}

Qui a codé ce superbe contenu ?

Arthur Weill

Arthur Weill - Directeur - Tainix et Web and Cow

J'accompagne mes clients dans leur stratégie digitale et mes collaborateurs.trices dans leur progression technique.


Keep learning

Autres contenus à découvrir


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.