Poprzedni rozdział: Metody magiczne
Następny rozdział: Automatyczne ładowanie

Iteratory edytuj

Na samym początku naszej nauki poznaliśmy pętlę foreach, która przechodzi po wszystkich elementach tablicy. Okazuje się, że możemy ją stosować także do iterowania po obiektach, a co więcej - możemy kontrolować, co właściwie nasz obiekt będzie wtedy generować! Obiekty z taką spersonalizowaną obsługą pętli foreach nazwiemy iteratorami, a omówienie zagadnienia rozpoczniemy od pokazania paru przykładów ze znanej już nam biblioteki Standard PHP Library, która posiada całkiem pokaźną kolekcję domyślnych iteratorów.

Iteratory z biblioteki SPL edytuj

Przed zabraniem się za programowanie obiektowe omawialiśmy m.in. funkcje dostępu do plików. Korzystanie z nich było średnio wygodne. Przykładowo, aby odczytać zawartość katalogu, musieliśmy manipulować dużą ilością funkcji, a na dokładkę otrzymywaliśmy jedynie nazwę, nie wiedząc nawet czy mamy do czynienia z plikiem czy z katalogiem. Iteratory doskonale nadają się do operacji na systemie plików. W SPL-u mamy klasę DirectoryIterator, która pozwoli nam pobrać zawartość katalogu:

<?php
try
{
   $dir = new DirectoryIterator('./katalog/');
   foreach($dir as $file)
   {
      // Pomiń pozycje "." oraz ".."
      if($file->isDot())
      {
         continue;
      }
      if($file->isDir())
      {
         echo 'Katalog '.$file.'<br/>';
      }
      else
      {
         echo 'Plik '.$file.'<br/>';
      }
   }
}
catch(UnexpectedValueException $exception)
{
   echo 'Błąd: '.$exception->getMessage();
}

Teraz iteracja po katalogach jest wyjątkowo prosta, a na dodatek zyskujemy czytelne, obiektowe API. Gdyby katalog nie istniał, zamiast dziwacznych komunikatów dostaniemy normalny wyjątek, który możemy przechwycić i oprogramować według naszych potrzeb.

Zastanówmy się teraz, jak wyświetlić katalogi wraz z zawartością ich podkatalogów, czyli innymi słowy - kompletne drzewo systemu plików. Jest to możliwe dzięki dwóm kolejnym iteratorom: RecursiveDirectoryIterator oraz RecursiveIteratorIterator. Powinny być one używane razem - pierwszy bowiem sam z siebie do podkatalogów nie wejdzie, ale udostępnia metody getChildren() i hasChildren(), które są wykorzystywane przez drugi. RecursiveIteratorIterator służy do rekurencyjnego przechodzenia po dowolnej strukturze, która implementuje powyższe dwie metody (także naszej własnej). My jednak wróćmy do naszych katalogów:

<?php
$dirIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('./s3/'), RecursiveIteratorIterator::SELF_FIRST);
foreach($dirIterator as $file)
{
   if($file->isDir())
   {
      echo str_repeat('---', $dirIterator->getDepth()).' Katalog '.$file.'<br/>';
   }
   else
   {
      echo str_repeat('---', $dirIterator->getDepth()).' Plik '.$file.'<br/>';
   }
}

Konstruktor klasy RecursiveIteratorIterator pobiera jako pierwszy argument obiekt iteratora, po którym należy przejść rekurencyjnie. W naszym przypadku jest to iterator do przechodzenia po katalogach. W drugim, opcjonalnym parametrze możemy ustawić sposób przechodzenia przy pomocy stałych klasowych:

  1. RecursiveIteratorIterator::LEAVES_ONLY - wyświetli jedynie liście (pliki). Wartość domyślna.
  2. RecursiveIteratorIterator::SELF_FIRST - najpierw zwróć katalog, a później to, co się w nim znajduje.
  3. RecursiveIteratorIterator::CHILD_FIRST - najpierw zwróć zawartość katalogu, a później sam katalog.

Iterator produkuje nam obiekty poszczególnych plików, lecz nie ma w nich informacji o ich głębokości w strukturze katalogowej. Tę informację uzyskujemy bezpośrednio z iteratora poprzez metodę getDepth(), dzięki czemu wiemy, ile pauz należy wyświetlić przed nazwą elementu, aby go odpowiednio wciąć.

Powyższy przykład pokazał jeszcze jedną ciekawą właściwość iteratorów SPL, a mianowicie możliwość ich komponowania w czasie wykonywania. Zauważmy, że nie mamy jednego iteratora do rekurencyjnego przechodzenia po katalogach, ale mamy dwa mniejsze - jeden oferujący rekurencję, drugi - wędrówki po systemie plików. Dopiero łącząc je ze sobą uzyskujemy to, co chcemy. Gdy przejdziemy do późniejszych rozdziałów podręcznika zauważymy, że takie postępowanie ma swoją fachową nazwę dekorator. Oto inne zastosowanie kompozycji iteratorów. Mamy tablicę z sześcioma elementami, ale chcemy wyświetlić jedynie elementy od 2 do 4, przy czym nie znamy ich indeksów (pętla for odpada). Wykorzystajmy zatem iterator tablicowy ArrayIterator i połączmy go z LimitIterator, który doda odpowiednie limity:

<?php
$array = new ArrayIterator(
   array('jabłko', 'banan', 'gruszka', 'wisienka', 'czereśnia', 'truskawka')
);

echo '<ul>';
foreach(new LimitIterator($array, 2, 2) as $item)
{
   echo '<li>'.$item.'</li>';
}
echo '</ul>';

Chociaż tablice współpracują z foreach, ale nie współpracują z innymi iteratorami, dlatego w tym przypadku musimy ją opakować w specjalny obiekt klasy ArrayIterator. Domyślnie pokazałby on nam wszystkie owoce, ale my chcemy dostać jedynie "gruszkę" oraz "wisienkę". Tu do akcji wkracza LimitIterator, któremu mówimy, że chcemy dostać dwa elementy (trzeci argument) począwszy od drugiego (drugi argument - liczone od zera!). Uruchom ten przykład i spróbuj pobawić się ustawieniami, aby zobaczyć, co się stanie.

Interfejs IteratorAggregate edytuj

Gdy wiemy już, ile można osiągnąć dzięki iteratorom, pora nauczyć się samodzielnie je tworzyć. Sprowadza się to do zaimplementowania w naszej klasie jednego z dwóch specjalnych interfejsów. Zaczniemy od omówienia prostszego IteratorAggregate, który wymaga dodania dokładnie jednej metody: getIterator(). Jej zadaniem jest... utworzenie iteratora, który zajmie się procesem iteracji w imieniu naszej klasy. Przypomnijmy sobie naszą klasę Config, która opcje konfiguracyjne przechowuje w tablicy. Dla celów debugowych chcielibyśmy mieć możliwość prostego wyświetlenia wszystkich opcji. Przypomnijmy sobie strukturę klasy:

<?php
class Config implements Countable
{
   private $_config = array();
   private $_awaitingLoaders = array();
 
   // metody klasy

} // end Config;

Opcje przechowywane są w polu $_config, dlatego to po nim powinniśmy iterować. Dodajmy do klasy interfejs IteratorAggregate oraz zaimplementujmy niezbędną metodę:

<?php
class Config implements Countable, IteratorAggregate
{
   private $_config = array();
   private $_awaitingLoaders = array();
 
   public function getIterator()
   {
      return new ArrayIterator($this->_config);
   } // end getIterator();

   // metody klasy
} // end Config;

Ponieważ lista opcji jest tablicą, w naszym imieniu iterować będzie po niej ArrayIterator, który tworzymy w momencie wywołania getIterator(). Możemy teraz wyświetlić wszystkie opcje:

<?php
require('./Config.php');

$config = new Config;
$config->addLoader(new FileConfigLoader('./config.ini.php'));

echo '<ul>';
foreach($config as $name => $value)
{
   echo '<li>'.$name.': '.$value.'</li>';
}
echo '</ul>';

Interfejs Iterator edytuj

Interfejs IteratorAggregate jest przydatny w najprostszych sytuacjach, gdy mamy pasujący iterator, ale pojawia się problem, skąd taki wziąć gdy żaden nam nie pasuje? Pełną kontrolę nad przebiegiem procesu iteracji zapewnia dopiero interfejs Iterator, który wymaga dodania do naszej klasy pięciu metod:

  1. reset() - ustawia kursor na początku kolekcji.
  2. valid() - sprawdza czy kursor znajduje się we właściwym położeniu (np. czy nie wyszedł poza rozmiar tablicy).
  3. next() - przesuwa kursor na kolejną pozycję.
  4. key() - zwraca klucz aktualnej pozycji.
  5. current() - zwraca wartość aktualnej pozycji.

Aby lepiej zrozumieć, jak PHP z nich korzysta, spróbujmy przetłumaczyć sobie wywołanie pętli foreach na odpowiadające jej wywołanie pętli while:

<?php
// Tworzymy dowolny iterator
$iterator = new SomeIterator;

// Foreach
foreach($iterator as $name => $value)
{
   ...
}

// Równoważny mu while
$iterator->reset();
while($iterator->valid())
{
   $name = $iterator->key();
   $value = $iterator->value();

   ...

   $iterator->next();
}

Wyposażeni w taką informację spróbujmy napisać iterator zliczający. Będzie on odliczać od podanej liczby zadaną ilość razy. Jako wartość będzie zwracana aktualna liczba, a jako klucz - numer iteracji.

<?php
class CountingIterator implements Iterator
{
   private $_start;
   private $_end;
   private $_key;
   private $_value;

   public function __construct($start, $offset)
   {
      $this->_start = $start;
      $this->_offset = $offset;
   } // end __construct();

   public function reset()
   {
      $this->_key = 0;
      $this->_value = $this->_start;
   } // end reset();

   public function valid()
   {
      return $this->_key < $this->_offset;
   } // end valid();

   public function next()
   {
      $this->_key++;
      $this->_value++;
   } // end next();

   public function key()
   {
      return $this->_key;
   } // end key();

   public function current()
   {
      return $this->_value;
   } // end current();
} // end CountingIterator;

echo '<ul>';
foreach(new CountingIterator(5, 10) as $idx => $value)
{
   echo '<li>'.$idx.'. '.$value.'</li>';
}
echo '</ul>';

Powyższy przykład wyświetli dziesięć kolejnych liczb, począwszy od wartości 5. Zwróćmy uwagę, że w ten sposób zmieniliśmy foreach w pętlę for. Oczywiście w prawdziwych aplikacjach WWW nie będzie to mieć zbyt wielkiego sensu, ale dobrze pokazuje istotę procesu iteracji. W ramach ćwiczenia spróbuj zaimplementować interfejs Iterator w miejsce IteratorAggregate w klasie Config. Będziesz musiał iterować po tablicy asocjacyjnej, dlatego wszystkie metody interfejsu będą zwykłymi nakładkami na analogiczne funkcje obsługi tablic: reset(), next(), key() oraz current(). Jeśli nie pamiętasz ich działania, posłuż się dokumentacją i spróbuj rozwiązać problem, jak w metodzie valid() sprawdzić, że dotarliśmy do końca tablicy.

Zakończenie edytuj

Iteratory są bardzo wygodnym elementem PHP pozwalającym ukryć przed programistą wiele zbędnych szczegółów technicznych. Jednak i tutaj należy korzystać z nich z umiarem i nie stosować, gdy nie ma ku temu żadnego logicznego uzasadnienia.