Poprzedni tekst traktujący o SPL rozpoczął serię wpisów poświęconych standardowej bibliotece PHP (Standard PHP Library). Poznaliśmy w nim trzy klasy ułatwiające pracę z plikiem. Podczas poznawania tych klas, okazało się, że korzystają z dobrodziejstw iteratorów oraz wbudowanych interfejsów. Dzisiaj skupimy się jedynie na interfejsach dostarczanych razem z SPL z wyłączeniem SplObserver oraz SplSubject. O obserwatorze napiszę innym razem. Obok interfejsów dostępnych w ramach SPL, PHP oferuje kilka dodatkowych interfejsów, o których również dzisiaj napiszę.
Traversable
Jest to pusty interfejs służący do oznaczenia klasy jako “iterowalnej”. Każdy obiekt klasy implementującej interfejs Traversalbe może zostać użyty w pętli foreach jako wyrażenie. Interfejs ten nie może zostać użyty bezpośrednio w klasie. Zamiast tego należy skorzystać z interfejsu dziedziczącego po Traversable, np Iterator lub IteratorAggregate.
Iterator
Interfejs ten służy jako rozwinięcie idei interfejsu Traversable. Wprowadza on metody, które zostaną wykorzystane przez pętlę foreach podczas iteracji po obiekcie. Domyślnie pętla foreach przejdzie po wszystkich właściwościach publicznych obiektu. W przypadku użycia interfejsu wykorzystane zostaną metody:
- current – zwraca bieżący element
- key – zwraca klucz aktualnego elementu
- next – przesuwa wewnętrzny wskaźnik na kolejny element
- rewind – przesuwa wewnętrzny wskaźnik na pierwszy element
- valid – sprawdza czy bieżąca pozycja jest poprawna
Zobaczmy przykład prostej klasy korzystającej z tego interfejsu
class Foo implements Iterator
{
protected $_position;
protected $_someData = array('123', 'abc', 'qwe');
public function __construct()
{
$this->_position = 0;
}
public function current()
{
return $this->_someData[$this->_position];
}
public function key()
{
return $this->_position;
}
public function next()
{
++$this->_position;
}
public function rewind()
{
$this->_position = 0;
}
public function valid()
{
return isset($this->_someData[$this->_position]);
}
}
$foo = new Foo();
foreach($foo as $key => $value) {
echo $key . ' = ' . $value;
}
Zgodnie z oczekiwaniami, wynikiem działania będzie
0 = 123 1 = abc 2 = qwe
Po co nam klasa, która robi to samo co tablica? Odpowiedź jest tylko jedna – po nic. Interfejs ten ma zastosowanie wszędzie tam, gdzie mamy do czynienia z kolekcjami obiektów, na których będziemy wykonywać dodatkowe operacje. Może to być na przykład kolekcja złożona z obiektów, gdzie każdy z nich odpowiada wierszowi w bazie danych.
IteratorAggregate
Interfejs ten jest o tyle ciekawy, że pozwala “oddelegować” iterowanie po danym obiekcie do innej klasy implementującej interfejs Traversable (lub inny interfejs po nim dziedziczący).
class Foo implements Iterator
{
protected $_position = 0;
protected $_someData = array();
public function __construct($data)
{
$this->_someData = $data;
$this->rewind();
}
public function current()
{
return $this->_someData[$this->_position];
}
public function key()
{
return $this->_position;
}
public function next()
{
$this->_position = $this->_position + 1;
}
public function rewind()
{
$this->_position = 0;
}
public function valid()
{
return isset($this->_someData[$this->_position]);
}
}
class Bar implements IteratorAggregate
{
protected $_data = array('123', 'abc', 'qwe');
public function getIterator()
{
return new Foo($this->_data);
}
}
$bar = new Bar();
foreach($bar as $key => $value) {
echo $key . ' = ' . $value;
}
Do czego jest to nam potrzebne? Przykładów zastosowania jest co najmniej kilka. Najprostszym z nich jest udostępnienie chronionych lub prywatnych danych na zewnątrz.
class Foo implements IteratorAggregate
{
protected $_data = array(123, 'abc', 'qwe');
public function getIterator()
{
return new ArrayIterator($this->_data);
}
}
$foo = new Foo();
foreach($foo as $key => $value) {
echo $key . ' = ' . $value;
}
Nie przejmujcie się klasą ArrayIterator, omówię ją przy okazji iteratorów. Wystarczy, że będziecie wiedzieć, iż klasa ta pozwala na modyfikowanie i usuwanie kluczy i wartości tablic oraz obiektów.
Countable
Implementacja tego interfejsu oznacza, iż naszą klasę można policzyć. Interfejs definiuje tylko jedną publiczną metodę, która zwraca liczbę opisującą ilość elementów w naszej klasie.
class Foo implements Countable
{
protected $_counter = 0;
public function Bar()
{
/* coś co powoduje ustaienie licznika */
$this->_counter = 4;
}
public function count()
{
return $this->_counter;
}
}
$foo = new Foo();
$foo->Bar();
echo count($foo);
Do czego jest nam potrzebny ten interfejs? Zastosowań jest tyle ilu programistów z niego korzystających. Najlepszym przykładem będzie tutaj klasa paginatora, która w liczniku może przechowywać ilość stron.
OuterIterator
Przeznaczeniem tego interfejsu jest po prostu być. Nie spotkałem się jeszcze nigdy z jego praktycznym zastosowaniem. Jedyną rolę jaką spełnia, jest bycie nadrzędnym interfejsem dla szeregu iteratorów. Definiuje on jedną metodę (poza tymi, które dziedziczy) – getInnerIterator, zwracającą wewnętrzny iterator.
RecursiveIterator
Podobnie jak w poprzednim przypadku, ten interfejs również powstał z myślą o iteratorach z niego korzystających. Bardzo rzadko będziemy z niego korzystać bezpośrednio, ponieważ całą brudną robotę wykonują za nas iteratory. Zadaniem tego interfejsu jest udostępnienie dwóch metod – hasChildren oraz getChildren. Pierwsza sprawdza, czy bieżący element posiada elementy potomne, druga zwraca te elementy. Zastosowaniem interfejsu RecursiveIterator jest umożliwienie iterowania po zagnieżdżonych kolekcjach bez konieczności zagłębiania się w ich strukturę. Innymi słowy wystarczy jedna pętla, aby przejść po wszystkich elementach zagnieżdżonej kolekcji.
SeekableIterator
Interesujący interfejs umożliwiający wyszukiwanie w obiekcie. Definiuje jedną metodę – seek – przyjmującą jako argument pozycję, z której chcemy pobrać dane.
class Foo implements SeekableIterator
{
protected $_position = 0;
protected $_someData = array(
'123',
'abc',
'qwe'
);
public function __construct()
{
$this->rewind();
}
public function current()
{
return $this->_someData[$this->_position];
}
public function key()
{
return $this->_position;
}
public function next()
{
$this->_position = $this->_position + 1;
}
public function rewind()
{
$this->_position = 0;
}
public function valid()
{
return isset($this->_someData[$this->_position]);
}
public function seek($position)
{
$this->_position = $position;
}
}
$foo = new Foo();
$foo->seek(2);
echo $foo->current();
W przypadku metody seek należy pamiętać o dodaniu sprawdzenia, czy pozycja rzeczywiście istnieje w naszej kolekcji. W przeciwnym wypadku nasza aplikacja może działać niepoprawnie.
ArrayAccess
Interfejs ArraAccess pozwala na korzystanie z zapisu zarezerwowanego dla tablic w kontekście obiektu. Definiuje on cztery metody
- offsetExists – sprawdza, czy w obiekcie istnieje wskazany klucz
- offsetGet – pobiera wartość dla wskazanego klucza
- offsetSet – ustawia wartość dla wskazanego klucza
- offsetUnset – usuwa klucz wraz z jego wartością
class Foo implements ArrayAccess
{
protected $_someData = array(
'key1' => '123',
'key2' => 'abc',
'key3' => 'qwe'
);
public function offsetExists($offset)
{
return isset($this->_someData[$offset]);
}
public function offsetGet($offset)
{
return $this->_someData[$offset];
}
public function offsetSet($offset, $value)
{
$this->_someData[$offset] = $value;
}
public function offsetUnset($offset)
{
unset($this->_someData[$offset]);
}
}
$foo = new Foo();
var_dump(isset($foo['key1'])); // true
echo $foo['key1']; // 123
$foo['key1'] = 456;
echo $foo['key1']; // 456
unset($foo['key1']);
print_r($foo);
/*
123456Foo Object
(
[_someData:protected] => Array
(
[key2] => abc
[key3] => qwe
)
)
*/
Możliwość korzystania z obiektu tak, jakby to była tablica daje duże możliwości podczas tworzenia obiektów, które powinny mieć jeden interfejs dostępu do danych, niezależnie od ich źródła pochodzenia. Dobrym przykładem będzie tutaj klasa konfiguracji, która może korzystać z danych zapisanych w postaci tablicy PHP, pliku tekstowego, bazy danych, czy dowolnego innego nośnika danych. Zastosowanie ArrayAccess spowoduje, że dostęp do tych danych zawsze będzie odbywał się w ten sam sposób.
Serializable
Ostatnim dostępnym interfejsem jest Serializable. Jest to rozszerzona wersja magicznych metod __sleep i __wakeup. Interfejs definiuje dwie metody: serialize oraz unserialize. Pierwsza z nich jest wykonywana w momencie serializacji obiektu i musi zwracać string. Druga metoda wywoływana jest w momencie deserializacji obiektu. Daje to możliwość zaimplementowania własnego mechanizmu serializacji i deserializacji.
class Foo implements Serializable
{
private $_field = '123';
public function serialize()
{
return serialize($this->_field);
}
public function unserialize($serialized)
{
$this->_field = unserialize($serialized);
}
}
$foo = new Foo();
$serialized = serialize($foo);
$unserialized = unserialize($serialized);
Podsumowanie
W dzisiejszym wpisie skupiliśmy się na interfejsach dostępnych w PHP (nie tylko w ramach SPL). Część z nich nie nadaje się do bezpośredniego użytku, a niektóre wydają się być pozbawione racji bytu, ponieważ duplikują funkcjonalności dostępne w PHP od dłuższego czasu. Dopiero użyte razem i przekształcone do postaci klas z krwi i kości – iteratorów – ujawniają do czego są naprawdę zdolne. Mimo wszystko warto jest poznać, aby wiedzieć dlaczego ta klasa działa tak, a nie inaczej.
Zapomniałeś jeszcze o DirectoryIterator & friends, fajnie by się to połączyło w całość w kontekście poprzedniego wpisu :>
DirectoryIterator to już gotowy iterator, o którym będę pisał w niedalekiej przyszłości. Temat iteratorów jest na tyle obszerny, że będę musiał go podzielić na kilka wpisów. Docelowo postaram się o jeden zgrabny dokument zawierający opis całego SPL.
Czyli pewnie jeszcze czekają nas struktury danych ? Heap, LinkedList etc ?
Tak. Wszystkie zagadnienia związane z SPL podzielone na logiczne grupy. W przypadku interfejsów celowo pominąłem SplObserver i SplObject, by napisać o nich osobny wpis. O SPL będzie jeszcze kilka wpisów o iteratorach (w jednym się nie zmieści), na pewno jeden będzie o strukturach danych, jeden o wyjątkach i jeden o autoloaderze. W sumie będzie jeszcze około 5-7 wpisów, które na sam koniec zbiorę w jeden dokument i opublikuję na blogu.
Imponujący kawał dobrej roboty. Fajnie przeczytać kompletny artykuł poza samym manualem, gdzie wszystkie elementy SPLa zostały omówione i skomentowane. Czekam na kolejne wpisy!
Świetna robota! Tak trzymać.
batmanie, fajna seria wpisów i wyczekuję kolejnych części
choć SPL większość zna ze słyszenia to mało który wykorzystuje w praktyce, także szerz co dobre.
Kamil… Nie przesadzajmy
Taki DirectoryIterator to fajne rozwiązanie dla mniejszych serwisów, gdzie wydajność nie jest tak ważna, a pisanie od zera tego co on implementuje to trochę pracy jest dla początkującego. Sam czasem SPL wykorzystuję, bo też i o podobnych rozwiązaniach wiedziałem z C++, gdzie istnieje STL oraz algo.h implementujące choćby listy, wektory, hashmapy czy iteratory właśnie. Ogólnie artykuły mi się podobają z tej serii, ponieważ nie są przeładowane suchą wiedzą oraz opisane w sposób przystępny dla początkujących. A najlepsze są chyba komentarze o przydatności danego rozwiązania. Po co bowiem ktoś ma kombinować jak je wykorzystać, skoro z góry można mu powiedzieć: "Implementacja tego nie ma sensu. Implementuj klasy od niej pochodne."
~thek
Interfejsy musiały zostać opisane tytułem wstępu do iteratorów, które z nich namiętnie korzystają. Praktyczna wiedza dopiero się zacznie.
Dlaczego DirectoryIterator nie nadaje się do dużych serwisów? Przy dużej ilości plików różnice w czasie działania skryptu są znikome. Przy okazji opisywania iteratorów, skuszę się na małe porównanie i sprawdzimy jako to jest
@thek
Nie przesadzajmy też w drugą stronę
Bardziej od wydajności tego rzędu (bo to raczej niewielkie optymalizacje) liczy się komfort i szybkość pracy.
I zamiast męczyć się ze standardowymi funkcjami PHP, możemy w tym czasie zaimplementować buforowanie czy poszukać miejsc, które naprawdę wymagają optymalizacji.
Gdyby tak wszystko optymalizować ile wlezie to szybko wrócilibyśmy do strukturalnego pisania.
@Kamil Brenk:
A czy tak nie robisz gdy już masz gotową aplikację i szukasz jej wąskich gardeł? Z czasem, jeśli aplikacja ma być wydajna, musisz zagłębić się i zastępować pewne struktury innymi, prostszymi i mniej przez to obciążającymi. Wiadomo, że ma to sens tylko dla pewnej części aplikacji, najbardziej wpływającej na wydajność, mającej największy wpływ na czas działania aplikacji.
@batman:
Pracuję często z zagnieżdżonymi katalogami, które posiadają po kilka tysięcy plików. Samo wejście przy użyciu ftp do takiego potrafi nieźle go zmulić i czekasz długo na przesłanie listy plików w nim użycie RecursiveDirectory Iterator potrafi zajechać wtedy każdy skrypt. Nawet zwykły DirectoryIterator ma tutaj czas liczenia średnio przynajmniej o połowę gorszy od zwykłych operacji na katalogach. Miałem bardzo często sytuację, gdzie DirectoryIterator mielić potrafił to kilka razy dłużej.
Dobry wpis. Rozwijaj dalej temat SPL