janos.szurovecz.hu

A régi blogom, ahova lehet írok majd még...

Iterátorok PHP-ben

2011-02-19 4 perc olvasási idő

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).

comments powered by Disqus