FilterIterator i DirectoryIterator
Autor: Milan Popović |
Datum:
  • # PHP Science

U prethodnom članku smo se upoznali sa predefinisanim PHP iterator interfejsima. Sada je vreme da proučimo neke od izvedenih klasa ovih interfejsa. Prvi u nizu su FilterIterator i DirectoryIterator SPL klase. Najpre ćemo se upoznati sa osnovnim konceptima ovih klasa, dati jednostavne primere da bismo na kraju napraviti sistem u kome ćemo koristiti obe klase.

FilterIterator

FilterIterator je izvedena klasa Iterator interfejsa. Dostupan je od verzije PHP 5.1.0. Pripada grupi dekoratora u SPL. Uloga FilterIteratora jeste da odstrani (filtrira) iz iteracije neželjene elemente. Dakle umesto da uslove postavljamo unutar bloka za iteraciju elemenata mi ćemo to učiniti unutar ovog iteratora. Prilikom instanciranja objekta klase koja nasleđuje ovu klasu, konstruktoru prosleđujemo iterator koji smo ranije definisali. Logika filtriranja je smeštena u metodi accept(). U ovoj metodi se definiše pravilo filtera. Ona vraća bool vrednost koja predstavlja informaciju da li je tekući element u iteraciji prošao postavljene filtere. Ako jeste, tekući element internog iteratora će biti dostupan, u suprotnom ne.

abstract FilterIterator extends IteratorIterator implements OuterIterator , Traversable , Iterator {
  /* Methods */
  abstract bool accept ( void )
  __construct ( Iterator $iterator )
  mixed current ( void )
  Iterator getInnerIterator ( void )
  mixed key ( void )
  void next ( void )
  void rewind ( void )
  bool valid ( void )
}

Hajde da uzmemo kao primer niz brojeva od 1 do 16. Koristićemo klasu FilterExample da bismo filtrirali zadati niz. Tom prilikom ćemo prosleđivati koji tip filtera hoćemo tj da li želimo da ispišemo sve neparne ili parne brojeve.

class FilterIteratorExample extends FilterIterator
{
  protected $number_type;

  public function __construct($iterator, $number_type)
  {
      parent::__construct($iterator);
      $this->number_type = $number_type;
  }

  public function accept()
  {
      switch ($this->number_type)
      {
          case 'even':
              return ! ($this->current() % 2);
              break;
          case 'odd':
              return ($this->current() % 2);
          default:
              return false;
              break;
      }
      return false;
  }
}

$numbers = range(1, 16);
$numbers_iterator = new ArrayIterator($numbers);
$filtered = new FilterIteratorExample($numbers_iterator, 'even');
foreach ($filtered as $variable => $value)
{
  echo $value.'
'; }

Kao rezultat izvršenja dobili smo samo parne odnosno samo neparne članove prosleđenog niza.

Ulazni i izlazni element iz FilterIteratora je takođe iterator što znači da možemo pozvati više filtera uzastopno. Hajde da prethodni primer proširimo tako što ćemo dodati još dva filtera - da se prikazuju samo brojevi deljivi sa 3 odnosno sa 4.  Naša klasa sada izgleda ovako

class FilterExample extends FilterIterator
{
   protected $number_type;
   public function __construct($iterator, $number_type)
   {
   parent::__construct($iterator);
   $this->number_type = $number_type;
   }
   public function accept()
   {
   switch ($this->number_type)
   {
   case 'even':
   return ! ($this->current()%2);
   break;
   case 'odd':
   return ($this->current()%2);
   case 'threemod':
   return ! ($this->current()%3);
   case 'fourmod':
   return ! ($this->current()%4);
   default:
   return false;
   break;
   }
   return false;
   }
}
$numbers = range(1, 16);
$numbers_iterator = new ArrayIterator($numbers);
$filtered = new FilterExample($numbers_iterator, 'even');
$filtered = new FilterExample($filtered, 'threemod');
$filtered = new FilterExample($filtered, 'fourmod');
foreach ($filtered as $variable => $value)
{
   echo $value.'
'; }

Kao rezultat izvršenja dobili smo broj 12 budući da jedino on ispunjava uslov sva tri filtera.

U ovom slučaju smo filtrirali niz koristeći samo jednu klasu, dok u praksi možemo (a to ćemo često i činiti) pozivati više različitih klasa koje su nasledile FilterIterator.

Kada ne bismo koristili FilterIterator prethodni primer izgledao bi ovako

$numbers = range(1, 16);
$numbers_iterator = new ArrayIterator($numbers);
foreach ($filtered as $variable => $value)
{
   if ( $value %2 == 0 and $value %3 == 0 and $value %4 == 0)
   {
       echo $value.'
';    } }

Šta je ovde problem? Odgovor je jasan - mesto gde se čuva logika filtriranja. U ovom primeru definisana je direktno u petlji, dok smo je ranije smeštali u klase koje nasleđuju FilterIterator-e. Ako preterano složenu logiku definišemo direktno u petlji, kod postaje teško održiv i nerazumljiv. Često se dešava da programeri prosleđuju podatke iz kontrolera u view nad kojima kasnije vrše iteraciju i unutar te iteracije neretko i neko složeno filtriranje tih podataka. U tom slučaju bolje je napraviti dodatni FilterIterator čime ćemo izbeći to da logika bude smeštena u view. Samu logiku filtriranja možemo razbiti na više manjih, višestruko upotrebljivih celina, a FilterIteratori nam upravo to omogućuju.

Directory iterator

DirectoryIterator extends SplFileInfo implements Iterator , Traversable , SeekableIterator {
/* Methods */
public __construct ( string $path )
public DirectoryIterator current ( void )
public int getATime ( void )
public string getBasename ([ string $suffix ] )
public int getCTime ( void )
public string getExtension ( void )
public string getFilename ( void )
public int getGroup ( void )
public int getInode ( void )
public int getMTime ( void )
public int getOwner ( void )
public string getPath ( void )
public string getPathname ( void )
public int getPerms ( void )
public int getSize ( void )
public string getType ( void )
public bool isDir ( void )
public bool isDot ( void )
public bool isExecutable ( void )
public bool isFile ( void )
public bool isLink ( void )
public bool isReadable ( void )
public bool isWritable ( void )
public string key ( void )
public void next ( void )
public void rewind ( void )
public void seek ( int $position )
public string __toString ( void )
public bool valid ( void )
}

Dostupan je od verzije PHP 5.0. Pripada grupi konkretnih iteratora. Koristeći ovu SPL klasu možemo na najlakši mogući način kreirati iterator nad elementima direktorijuma. Prilikom iteracije u foreach petlji informacije o svim elementima direktorijuma (fajlovi i poddirektorijumi) dostupne su kao SplFileInfo objekti. Hajde da odmah napišemo jednostavan primer. Uzećemo kao primer direktorijum na lokaciji /var/www/blogsamples/disamples. Neka u tom direktorijumu imamo dostupne sledeće elemente:

  • first_file.php
  • second_file.php
  • txt_example.txt
  • first_subfolder
  • second_subfolder

Ako napišemo sledeću skriptu

$path = new DirectoryIterator('/var/www/blogsamples/disamples');

foreach ($path as $file)
{
   echo $file->getFilename().'
'; }

Kao rezultat izvršenja dobićemo spisak svih fajlova i foldera unutar direktorijuma koji smo uzeli kao primer

second_file.php
txt_example.txt
.
first_file.php
second_folder
first_folder
..

Pored očekivanih rezultata primećujete da su dostupni i takozvani “dot fajlovi” - “.” i “..” koji predstavljaju konfiguracione fajlove operativnog sistema. Kao što ste primetili koristili smo metodu getFilename() da bismo dobili informacije o nazivu elementa unutar direktorijuma. Pored ove izdvojio bih i sledeće metode

Naziv metode Opis metode
getATime ( void ) Vraća vreme poslednjeg pristupa fajlu
getBasename ([ string $suffix ] ) Vraća ime fajla bez informacije u putanji do fajla. Ako se prosledi i opcioni parametar (string tipa) i ukoliko se osnovno ime fajla završava upravo sa ovim parametrom, tada će se kao rezultat izvršenja vratiti naziv skraćen za prosleđeni parametar
getCTime ( void ) Vraća vreme poslednje izmene fajla u UNIX Timestamp formatu
getGroup ( void ) Vraća ID fajl grupe kojoj fajl pripada. Sve informacije o grupi možete dobiti preko posix_getgrgid(groupID) funkcije.
getExtension ( void ) Vraća naziv ekstenzije fajla
getFilename ( void ) Vraća ime fajla bez informacije u putanji do fajla. Razlikuje se od metode getBasename() po tome što ne prima opcioni parametar.
getMTime ( void ) Vraća vreme kada je sadržaj fajla poslednji put menjan.
getOwner ( void ) Vraća ID korisnika kome fajl pripada. Sve informacije o korisniku možete dobiti preko posix_getpwuid(userID) funkcije.
getPath ( void ) Vraća putanju do direktorijuma u kome se fajl nalazi bez separatora za direktorijum na kraju naziva.
getPathname ( void ) Vraća punu putanju do fajla (uključujući i naziv fajla)
getPerms ( void ) Vraća nivo dozvola za rad sa fajlom.
getSize ( void ) Vraća veličinu fajla u bajtovima
getType ( void ) Vraća tip fajla (može biti npr “file” ili “dir”)
isDir ( void ) Vraća odgovor da li je tekući fajl zapravo direktorijum.
isFile ( void ) Vraća odgovor da li je tekući fajl zaista fajl (ili nešto drugo).
isExecutable ( void ) Vraća odgovor da li je tekući fajl izvršan.
isReadable ( void ) Vraća vrednost da li je moguće čitati tekući fajl ili ne.
isWritable ( void ) Vraća vrednost da li je moguće upisati podatke u tekući fajl.
isDot (void) Vraća vrednost da li je tekući faj zapravo dot fajl.

Hajde da pogledamo kako bi mogli napisati skriptu koja radi ovu istu stvar ali bez korišćenja iteratora

$directory = "/var/www/blogsamples/disamples/";
if (is_dir($directory) and $openedDirectory = opendir($directory))
{
   while (($filename = readdir($openedDirectory)) !== false)
   {
   $info = new SplFileInfo($directory.$filename);
   echo $info->getFilename()."
";    }    closedir($openedDirectory); }

Primećujete da nam je za istu stvar potrebno neuporedivo više koda, pritom smo morali sami da instanciramo SplFileInfo klasu.

Primer kombinacije FilterIterator-a i DirectoryIterator-a

Sada ćemo na jednom primeru sa kojim se možete sresti u praksi da pokažemo primer korišćenja oba iteratora. Postavićemo jednostavan zadatak - prikazati sve fajlove koji imaju ekstenziju php unutar prosleđenog direktorijuma. Hajde da iskoristimo DirectoryIterator da rešimo ovaj problem

$path = new DirectoryIterator('/var/www/blogsamples/disamples');

foreach ($path as $file)
{
  if ($file->getExtension() == 'php')
  {
  echo $file->getFilename().'
';   } }

Kao rezultat ova skripta je prikazala upravo ono što smo  tražili. Međutim, šta ako dodamo još jedan uslov ili promenimo postojeći? Stvorićemo složenu logiku unutar petlje a to je nešto što bismo hteli da izbegnemo. Cilj nam je da tokom iteracije odstranimo određene elemente. To nas navodi na to da možemo primeniti FilterIterator. Hajde da ga priključimo postojećem primeru.

class FilterExtension extends FilterIterator
{
  public function __construct (Iterator $iterator, $filetype)
  {
    parent::__construct($iterator);
   $this->filetype = $filetype;
  } 
 public function accept()
 {
  return ($this->current()->getExtension() == $this->filetype);
 }
}
$path = new DirectoryIterator('/var/www/blogsamples/disamples');
$onlyPhpFiles = new FilterExtension($path, 'php');
foreach ($onlyPhpFiles as $file)
{
  echo $file->getFilename().'
'; }

Dobili smo isti rezultat dok smo logiku filtriranja smestili tamo gde joj je i mesto - u klasu koja je nasledila FilterIterator.

Primeri korišćenja

Dosta projekata koriste FilterIterator i DirectoryIterator kao i njihovu kombinaciju. Zanimljiv je primer Symfony Finder komponente koja pored FilterIterator-a i DirectoryIterator-a, kombinuje i veliki broj drugih iteratora. Omogućava izuzetno lagan način za pronalaženje fajlova i direktorijuma. Podržava filtriranje po imenu, veličini, datumu izmene, šablonu, veličini i još nekoliko kriterijuma. Moguće je izabrati nivo do kog želimo da prikažemo fajlove tj. da li hoćemo da prikažemo fajlove i iz poddirektorijuma. Fajlovi se mogu sortirati po imenu, tipu, vremenu poslednje izmene i drugim kriterijumima. Glavna klasa komponente je Finder čija instanca je takođe iterator čijom iteracijom dobijamo SplFIleInfo objekte. Ako bismo želeli da izlistamo sve php fajlove u nekom direktorijumu koji su veći od 10K a kreirani su u poslednjih 30 dana koristeći ovu komponentu  uradili bismo to na sledeći način

$finder = new \Symfony\Component\Finder\Finder();
$iterator = $finder
->files()
->name('*.php')
->depth(0)
->date('since 30 days ago')
->size('>= 10K')
->in('/var/www/blogsamples/disamples/');
foreach ($iterator as $file)
{
   var_dump($file->getRealpath());
}

U Magentu možemo na više mesta videti upotrebu ova dva iteratora. Na primer u klasi Mage_Backup_Filesystem_Iterator_Filter koja nasleđuje FilterIterator imamo klasičan primer primene FilterIteratora

public function accept()
   {
   $current = $this->current()->__toString();
   $currentFilename = $this->current()->getFilename();

   if ($currentFilename == '.' || $currentFilename == '..') {
   return false;
   }

   foreach ($this->_filters as $filter) {
   if (false !== strpos($current, $filter)) {
   return false;
   }
   }

   return true;
   }

U klasi Mage_Captcha_Model_Observer metoda deleteExpiredImages izgleda ovako:

public function deleteExpiredImages()
   {
   foreach (Mage::app()->getWebsites(true) as $website){
   $expire = time() - Mage::helper('captcha')->getConfigNode('timeout', $website->getDefaultStore())*60;
   $imageDirectory = Mage::helper('captcha')->getImgDir($website);
   foreach (new DirectoryIterator($imageDirectory) as $file) {
   if ($file->isFile() && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'png') {
   if ($file->getMTime() < $expire) {
   unlink($file->getPathname());
   }
   }
   }
   }
   return $this;
   }

Kao što možete da vidite u pitanju je brisanje captcha fajlova čija je važnost istekla. Da bi se izlistali fajlovi iz direktorijuma sa slikama koristi se DirectoryIterator. Ono što bi moglo ovde da se izmeni jeste da se deo koji se nalazi unutar foreach petlje izmesti u posebnu klasu koja bi nasledila FilterIterator. To bismo uradili na sledeći način

class Expired_Files extends FilterIterator
{
   protected $expireTime;
   protected $filetype;

   public function __construct (Iterator $iterator, $expireTime, $filetype)
   {
   parent::__construct($iterator);
   $this->expireTime = $expireTime;
           $this->filetype = $filetype;
   }    
   
   public function accept()
   {
   if ($this->current()>isFile() &&
// takodje može da se zameni sa metodom getExtension() ali je ona dostupna od verzije PHP 5.3.6
   pathinfo($this->current()->getFilename(), PATHINFO_EXTENSION) == $filetype &&
   $this->current()->getMTime() < $this->expireTime
   )
   {
   return true;
   }
   return false;
   }
}

 Kada ovu klasu imamo na raspolaganju foreach petlju preko koje se fajlovi brišu možemo napisati:

$expiredPngFiles = new Expired_Files(new DirectoryIterator($imageDirectory), $expire, ‘png’);
foreach ($expiredPngFiles as $file) {
   unlink($file->getPathname());
}

Klasu Expired_Files možemo koristiti i u drugim slučajevima kad god nam treba filter za fajlove određenog tipa koji su nastali pre definisanog vremena.

Dalji koraci

Ukoliko već niste, pravo je vreme da dalje proučite gde sve možete primeniti FilterIterator i DirectoryIterator. Iteratori će vam omogućiti u potpunosti objektno orjentisano rešenje za probleme sa kojima se svakodnevno susrećete. Pred vama je veliki broj kombinacija iteratora koje smo obradili sa iteratorima kao što je LimitIterator, RecursiveDirectoryIterator, IteratorIterator i mnogim drugim. Odvojte malo vremena za “igru” sa njima - siguran sam da će vam se svideti i da ćete naći veliku korist koju možete iskoristiti u vašim budućim projektima.