T: / Articles techniques / PHP
Exemple d’implémentation de l’interface native de PHP Iterator.
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.
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 :
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 :
Puis la méthode addMany :
public function addMany(string $emails, string $separator = ';'): void
{
foreach (explode($separator, $emails) as $email) {
$this->addOne($email);
}
}
Quelques explications :
On aurait pu utiliser un array_map pour éviter le foreach, de cette façon :
array_map(
[$this, 'addOne'],
explode($separator, $emails)
);
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).
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 !
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é :
<?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]);
}
}
Other content to discover