Iterátorok PHP-ben
Minden programozónál előfordul az, hogy újra feltalál valamit. Így jártam én most az iterátorokkal. Természetesen tudom mi az az iterátor, és napi szinten használom őket, de egy mostani munkám során rájöttem, hogy bizonyos helyzetekben életmentők tudnak lenni. A cikkben bemutatok néhány PHP-ben elérhető iterátor interfészt, illetve osztályt, valamint készíteni is fogok két saját osztályt.
Feladat
Legyen a feladat a következő: Jelenítsük meg adott könyvtárban lévő összes alkönyvtárat, valamint azon fájlokat, amelyek .jpg, .JPG, .png, .PNG kiterjesztésűek. A megoldás CLI-ben jelenítse meg a hierarchiát.
Első lépések
Először csak járjuk be a teljes könyvtár hierarchiát és írjuk ki a fájlok nevét.
<?php
$directoryIterator = new RecursiveDirectoryIterator('..', FilesystemIterator::SKIP_DOTS);
$recursiveIteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursiveIteratorIterator as $node) {
echo $node->getFilename() . PHP_EOL;
}
A RecursiveDirectoryIterator
beépített osztály, a megadott könyvtárat lehet bejárni a segítségével. A konstruktorban beállítottam még, hogy a ‘.’ és a ‘..’ fájlokat hagyja ki. Ahhoz, hogy egy RecursiveIterator
interfészt implementáló osztályt (ilyen a RecursiveDirectoryIterator
is) egy ciklussal bejárhassuk, használhatjuk a RecursiveIteratorIterator
osztályt. Ez a paraméterben megadott módon járja be rekurzívan az iterátort és minden iterációban lekérhetjük például, hogy hanyadik szinten vagyunk. A kiíratás hatására megjelennek egymás alatt a fájlok.
Szűrés
A következő feladat legyen az, hogy csak a megfelelő fájlokat jelenítsük meg. Itt rögtön jöhet a triviális megoldás, miszerint a ciklus törzsében egy if szerkezettel ellenőrizzük az aktuális fáljt. Ez itt még működik is, de minél többféle és minél hosszabb, és minél többször ismétlődő ilyen feltétel vizsgálatokat írunk, annál inkább megéri ezeket iterátorokban megoldani. Miről is van szó.
A PHP-ben van egy RegexIterator
osztály, ami a FilterIterator
osztályból származik. Utóbbiban az accept()
metódus minden iterációban meghívódik és az általa visszaadott bool
érték dönti el, hogy az adott érték megfelelő-e. Ha nem, akkor azonnal a következőre ugrik. A RegexIterator
osztály ezt használja ki: reguláris kifejezésre vizsgál illeszkedést. Ebből létrehoztam egy egyszerű fájl kiterjesztést ellenőrző osztályt:
<?php
namespace iterators;
class ExtensionIterator extends \RegexIterator {
public function __construct(\Iterator $iterator, array $validExtensions) {
parent::__construct($iterator, '/\.(' . implode('|', $validExtensions) . ')$/i');
}
}
Módosítsuk is a kódunkat:
<?php
require_once 'iterators/ExtensionIterator.php';
use iterators\ExtensionIterator;
$directoryIterator = new RecursiveDirectoryIterator('..', FilesystemIterator::SKIP_DOTS);
$recursiveIteratorIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
foreach (new ExtensionIterator($recursiveIteratorIterator, array('png', 'jpg')) as $node) {
echo $node->getFilename() . PHP_EOL;
}
A program lefutását követően láthatjuk, hogy megjelentek a képernyőn a megfelelő formátumú fájlok. Már csak a könyvtárakat kell megjeleníteni, valamint ráncba kell szedni a megjelenést.
Megjelenítés… helyett probléma
A megjelenítéshez használhatjuk a RecursiveTreeIterator
osztályt, azonban ez RecursiveIterator
-t vár konstruktorban. Ez érthető, viszont az előbb elkészített ExtensionIterator
Iterátor interfészt implementál. Vagyis ez azt jelenti, hogy “elvesztettük” a RecursiveDirectoryIterator
által adott rekurzivitást.
Sikerrel csak akkor fogunk járni, ha sikerül RecursiveIterator
objektumot készítenünk, ami egyben szűri a fájltípusokat. Valamint a már meglévő szűrő osztályunkat se szeretnénk átírni, se kidobni, ugyanis az tökéletesen működik, viszont nekünk többre van szükségünk. Készítsünk tehát egy osztályt, ami ebből származik és implementálja a RecursiveIterator
interfészt:
<?php
namespace iterators;
require_once 'iterators/ExtensionIterator.php';
class RecursiveExtensionIterator extends ExtensionIterator implements \RecursiveIterator {
private $validExtensions;
public function __construct(\RecursiveIterator $iterator, array $validExtensions) {
parent::__construct($iterator, $validExtensions);
$this->validExtensions = $validExtensions;
}
public function getChildren() {
return new self($this->getInnerIterator()->getChildren(), $this->validExtensions);
}
public function hasChildren() {
return $this->getInnerIterator()->hasChildren();
}
public function accept() {
return $this->getInnerIterator()->current()->isDir() || parent::accept();
}
}
A konstruktor nem túl érdekes, egyedül a type hintet módosítottam. Viszont bejött három metódus. A getChildren()
és a hasChildren()
az interfész miatt szükségesek. Szűrés esetén is akkor vannak egy fájlnak gyerekei, mint szűrés nélkül, így a hasChildren()
metódusban egyszerűen visszaadjuk a belső iterátor (az általunk használni kívánt RecursiveDirectoryIterator
) hasChildren()
kiértékelését. A getChildren()
metódusban viszont bejön a rekurzivitás: ha a belső iterátor getChildren()
metódusával térnénk vissza, akkor az alkönyvtárakban már nem működne a szűrés, ezért példányosítjuk újra az osztályt és ezzel térünk vissza (a getChildren()
-nek RecursiveIterator
-t kell visszaadnia). Már csak egy feladatunk van hátra: mivel az accept()
meghívódik abban az esetben is, amikor az aktuális elem egy könyvtár, ezért amennyiben az nem illeszkedik a reguláris kifejezésre, nem fogjuk listázáskor látni. Fogadjuk el, ha az könyvtár, vagy ha nem az, hívjuk meg az ősosztály accept()
metódusát, ami elvégzi az illeszkedés vizsgálatot.
Ha módosítjuk a kiíratást, akkor mind a könyvtárak, mind a képek megjelennek:
<?php
require_once 'iterators/RecursiveExtensionIterator.php';
use iterators\RecursiveExtensionIterator;
$directoryIterator = new RecursiveDirectoryIterator('..', FilesystemIterator::SKIP_DOTS);
$pictureFilterIterator = new RecursiveExtensionIterator($directoryIterator, array('png', 'jpg'));
$recursiveIteratorIterator = new RecursiveIteratorIterator($pictureFilterIterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursiveIteratorIterator as $node) {
echo $node->getFilename() . PHP_EOL;
}
Megjelenítés
Utolsó feladatként a formázás van hátra. Ehhez a RecursiveTreeIterator
-t használom.
<?php
require_once 'iterators/RecursiveExtensionIterator.php';
use iterators\RecursiveExtensionIterator;
$directoryIterator = new RecursiveDirectoryIterator('..', FilesystemIterator::SKIP_DOTS|FilesystemIterator::KEY_AS_FILENAME);
$pictureFilterIterator = new RecursiveExtensionIterator($directoryIterator, array('png', 'jpg'));
$treeIterator = new RecursiveTreeIterator($pictureFilterIterator, 0);
foreach ($treeIterator as $key => $node) {
echo $key . PHP_EOL;
}
A RecursiveDirectoryIterator
példányosításánál megadtam, hogy bejárásnál a kulcs a fájlnév legyen, így a RecursiveTreeIterator
-nál már felhasználhatom a kulcsot (alapból a második paraméter RecursiveTreeIterator::BYPASS_KEY
, ami nekünk pont nem jó).
Végszó
Szerintem jól szemlélteti ez a példa az iterátorok egymásba ágyazhatóságát, amivel végeredményben sokkal átláthatóbb kódot kapunk. Ha még egy szűrésre szükségünk lenne, az mindösszesen még egy sort jelentene (az osztály esetleges elkészítésén kívül).