Grâce à ce mécanisme, il est possible de faire des choses de ce style :
class myDirectory implements iterator
{
...
}
$directory = new myDirectory('/tmp');
foreach ($directory as $inode)
{
echo $inode;
}
Cerise sur le gâteau, il est également possible de filtrer ce qui est renvoyé par un objet de la classe iterator
à l'aide de l'itérateur filterIterator
, de la manière suivante :
class myDirectoryFilter extends filterIterator
{
...
}
$directory = new myDirectory('/tmp');
foreach (new myDirectoryFilter($directory) as $inode)
{
echo $inode;
}
C'est évidement très puissant.
Cependant, dans certain cas, il y a un problème.
Nous allons par exemple ajouter dans la classe myClass
une méthode getFromDirectory
qui prend en argument un objet de la classe myDirectory
et dont le but est d'effectuer un traitement, peu importe lequel, sur chaque fichier renvoyé par cet objet.
Son code est le suivant :
class myClass
{
public function getFromDirectory(myDirectory $directory)
{
if ($directory->exists())
{
foreach ($directory as $inode)
{
...
}
}
}
}
Tout va bien, sauf si nous souhaitons laisser au développeur la possibilité de filter ce que renvoie l'itérateur myDirectory
, indépendament de cet itérateur.
En effet, notre définition de la méthode oblige l'argument à être de la classe myDirectory
et non de la classe filterIterator
.
Qu'à cela ne tienne, nous allons définir un constructeur qui permet la définition du filtre à employer, et nous allons modifier la méthode myClass::getFromDirectory()
de la manière suivante :
class myClass
{
protected $filter = null;
public function __construct(filterIterator $filter)
{
$this->setFilter($filter);
}
public function setFilter(filterIterator $filter)
{
$this->filter = $filter;
return $this;
}
public function getFromDirectory(myDirectory $directory)
{
if ($directory->exists())
{
$this->filter->setInnerIterator($directory);
foreach ($this->filter as $inode)
{
...
}
}
}
...
}
Sauf que cela n'est pas possible de par la conception de la classe filterIterator
...
En effet, son constructeur impose que son argument soit un objet de la classe iterator
.
Cela revient donc à dire qu'il faut connaître à l'avance ce qui doit être filtré, ce qui n'est pas notre cas.
De plus, la méthode filterIterator::setInnerIterator()
n'existe pas.
Cerise sur le gâteau, la propriété filterIterator::$innerIterator
est une propriété privée de la classe, ce qui fait qu'il n'est pas possible de contourner le problème en dérivant filterIterator
.
Quelle est la solution, alors ?
Modifier notre définition de myClass::getFromDirectory()
pour qu'elle accepte un objet de la classe filterIterator
? Ce n'est pas jouable, puisque nous voulons pouvoir utiliser des méthodes appartenant à la classe myDirectory
dans cette méthode, en l'occurence myDirectory::exists()
.
Modifier la classe myDirectory
pour y encapsuler le filtrage ? Mauvaise idée, car pour chaque filtrage, il faudrait dériver la classe myDirectory
.
Je me suis finalement résigné à écrire ma propre classe afin de faire ce filtrage, en me basant sur le code source de filterIterator
afin de pouvoir découpler complétement le filtrage de l'itération.
J'ai ainsi rajouté une méthode myFilterIterator::setInnerIterator()
et j'ai modifié la signature du constructeur pour qu'il accepte la valeur null
par défaut.
J'ai donc dupliqué au moins 90% du code de filterIterator
, ce qui nous éloigne des bonnes pratiques... mais après tout, comme me l'a fait remarquer Jean-Marc, déclarer une propriété d'une classe pouvant être dérivée comme privée ne fait déjà pas partie des bonnes pratiques.
Mais si par hasard, vous avez une solution qui permette de découpler le filtrage de l'itération dans le cadre de cette problèmatique, en utilisant les outils natifs de la SPL, j'en serais très heureux, et peut être que cela me fera penser moins souvent, que PHP, c'est...
Et si, en plus, vous savez comment faire remonter ce genre de problème à l'auteur de la SPL, j'en serais encore plus heureux, car le bug tracker
ne semble pas être le meilleur endroit pour ce genre de choses.
7 réactions
1 De Hugo - 25/11/2008, 20:08
Merci pour ce billet très intéressant. Par contre dans tes premiers exemples de code, tu fais des extends des iterators à la place de faire des implements
2 De mageekguy - 26/11/2008, 11:28
@hugo : merci, erreur corrigée.
3 De pierre-alain bourdil - 26/11/2008, 13:30
voici un bout de code, qi permet a un utilisateur de définir un filterIterator sur ta methode getDirectory :
<?php
class FI extends FilterIterator{
function accept()
{
return true;
}
}
class OO{
function getFromDir(Iterator $myDirectory,$filter = null)
{
$md = $myDirectory;
if (!is_null($filter))
{
$md = new $filter($myDirectory);
}
echo "\ndebug ".__METHOD__. " mon iterator est un ".get_class($md);
}
}
class I implements Iterator
{
function rewind(){}
function next(){}
function current(){}
function key(){}
function valid(){}
}oo = new OO();
$oo->getFromDir(new I(),'FI');
4 De mageekguy - 27/11/2008, 09:21
@pierre-alain bourdil :
Bien vu !
Il est juste dommage avec ta méthode qu'on ne puisse pas définir le filtre une bonne fois pour toute et que l'on soit obligé de le passer en argument à chaque appel de la méthode.
5 De pierre-alain bourdil - 27/11/2008, 12:25
il est possible de transformer le parametre $filter en attribut de OO, et de l'initialiser au moment de l'instanciation de OO :
<code>
class OO{
protected $filter;
function __construct($filter=null)
{
$this->filter = $filter;
}
function getFromDir(Iterator $myDirectory)
{
$md = $myDirectory;
if (!is_null($this->filter))
{
$md = new $filter($myDirectory);
}
echo "\ndebug ".__METHOD__. " mon iterator est un ".get_class($md);
}
}
}
</code>
6 De mageekguy - 27/11/2008, 13:37
@pierre-alain bourdil :
Exact, mais si ton constructeur de filtre demande d'autres arguments, ce n'est plus jouable, à moins de rentrer dans des choses type call_user_func(), plutôt moche à mon sens. Bref, solution possible, mais pas suffisament générique.
Et je n'y avais pas pensé lors de la lecture de ton premier commentaire, mais ta première solution souffre du même défaut, vu que la création du filtre est encapsulée dans une méthode, le développeur n'a plus de contrôle sur cette création.
7 De Cédric - 19/12/2008, 10:59
Le problème est récurrent sur toutes les librairies qui se veulent génériques : soit elles sont trop difficiles à utiliser dans son contexte parce que très orientées vers un usage précis, soit elles sont trop génériques et deviennent complexe.
Problème de juste milieu. Dans le cas présent, je ne pense pas qu'un setIterator() ou qu'une propriété en protected au lieu de private auraient posé de gros soucis, ceci dit.
Y'a t'il déjà un référentiel de bonnes pratiques pour capitaliser sur ce genre d'expériences? En relation avec un forum de dev existant pour débattre un peu avant bien sûr?
ps: écrire son commentaire en gris clair sur blanc n'est pas super lisible, question d'ergonomie (merci firebug pour le hack au vol)