PHP Iterator interfejsi
Autor: Milan Popović |
Datum:
  • # PHP Science

PHP verzija 5.0 donela je određeni broj predefinisanih interfejsa:

Tema ovog članka je skup predefinisanih PHP iteratora, tj. Traversable, Iterator i IteratorAggregate interfejsi i njihova primena. Iteratori su jedna od funkcionalnosti PHP-a koju većina programera nedovoljno ili gotovo nikako ne koristi.

Za početak, kada naša klasa implementira neki od predefinisanih Iterator interfejsa, tada možemo izvršiti iteraciju nad instancom ove klase kao nad klasičnim nizom. Koristeći iteratore programer je u mogućnosti da opiše ponašanje objekata pri iteraciji u foreach petlji. Mogu biti:

  • Interni ili aktivni - sami definišu iteraciju
  • Ekterni ili pasivni - iteracija je definisana drugim, spoljnjim iteratorom

Možemo ih koristiti kada vršimo iteraciju bilo kojih elemenata koji sadrže sekvencu kao što su:

  • Vrednosti i ključevi u nizu
  • Linije teksta u fajlu
  • Rezultate upita nad bazom
  • Fajlove u direktorijumu
  • Elemente ili atribute u XML-u

Podrazumevana iteracija objekata

Možda niste upoznati sa činjenicom da PHP omogućava iteraciju bilo kog objekta. U praksi, to znači da ako definišemo jednostavnu klasu:

class Native
{
	public $first_variable = 'first';
	public $second_variable = 'second';
}

i deo koda kojim vršimo iteraciju objekta:

$instance = new Native();
foreach ($instance as $variable => $value)
{
    echo ”$variable - $value 
”;
}

kao rezultat izvršenja ovog koda dobijamo:

first_variable - first
second_variable - second

Ukoliko promenimo doseg promenljivih i postavimo ih na protected ili private - neće biti rezultata. Dakle, u ovoj takozvanoj "nativnoj" iteraciji objekata prikazuju se isključivo public promenljive. U praksi su promenljive uglavnom ili private ili protected jer želimo da sakrijemo, zaštitimo i kontrolišemo pristup podacima koji se nalaze u objektima, odnosno da ih enkapsuliramo i da njihovo čitanje i pisanje obavljamo preko posebnih metoda.

Traversable

Traversable {}

Ovo je osnovni interfejs koji ne sadrži ni jednu metodu. Služi kao osnova za IteratorAggregate i Iterator interfejse (pogledajte njihove sinopsise). Upravo Traversable interfejs ukazuje PHP engine-u da je iteracija nad objektima definisana iteratorom. Korisnički definisane klase ne mogu direktno implementirati ovaj interfejs već samo interfejse koji su iz njega izvedeni.

IteratorAggregate

IteratorAggregate extends Traversable 
{
    abstract public Traversable getIterator ( void );
}

Ovo je najjednostavniji iterator. Dovoljno je implementirati samo jednu metodu - getIterator(). Kada klasa implementira ovaj interfejs, prilikom iteracije nad objektima ove klase korišćenjem foreach petlje PHP engine zna da treba da pozove ovu metodu. Uzmimo kao primer sledeću klasu:

class IteratorAggregateExample implements IteratorAggregate
{
    protected $attributes = array('first', 'second');

    public function getIterator()
    {
        return new ArrayIterator($this->attributes);
    }
}

Kada izvršimo ovu skriptu:

$instance = new IteratorAggregateExample();
foreach ($instance as $variable => $value)
{
    echo ”$variable - $value 
”;
}

kao rezultat izvršenja dobijamo:

0 - first
1 - second

Lako se može primetiti da se objekat u iteraciji ponaša kao da vršimo iteraciju protected promenljive $attributes. Takođe, metoda getIterator() ne vraća niz $attributes već objekat klase ArrayIterator (izvedena klasa od Iterator iz standardne PHP biblioteke) kojoj smo u konstruktoru prosledili niz $attributes. Ovo je posledica toga što nam je u iteraciji potreban objekat koji implementira neki Iterator interfejs a ne niz. Klasa ArrayIterator implementira Iterator interfejs pa se zato koristi objekat ove klase. Ova vrsta iteratora je eksterni iterator upravo zato što se za iteraciju koristi neki drugi objekat.

Iterator

Iterator extends Traversable 
{
    abstract public mixed current ( void )
    abstract public scalar key ( void )
    abstract public void next ( void )
    abstract public void rewind ( void )
    abstract public boolean valid ( void )
}

Iterator interfejs je osnovni iterator. On je interni iterator budući da je iteracija opisana metodama koje moraju imati klase koje implementiraju ovaj interfejs, a to su: current(), key(), next(), rewind(), i valid(). Kada prethodni primer prepravimo da koristi ovaj interfejs i implementiramo navedene metode, jednostavna klasa bi izgledala ovako:

class IteratorExample implements Iterator
{
    protected $attributes = array('first', 'second');

    public function rewind()
    {
        reset($this->attributes);
    }

    public function current()
    {
        return current($this->attributes);
    }

    public function key()
    {
        return key($this->attributes);
    }

    public function next()
    {
        return next($this->attributes);
    }

    public function valid()
    {
        return false !== current($this->attributes);
    }
}

Kada pokrenemo skriptu za iteraciju nad objektom:

$instance = new IteratorExample();
foreach ($instance as $variable => $value)
{
    echo ”$variable - $value 
”;    
}

dobićemo isti rezultat kao u prethodnom primeru:

0 - first
1 - second

Kako izgleda proces iteracije ovakvog objekta? Počinje pozivom metode rewind() koja postavlja početni indeks za iteraciju. Nakon toga, poziva se metoda valid() koja će utvrditi da li je definisan trenutni indeks. U slučaju da jeste, poziva se metoda current() koja vraća vrednost trenutnog indeksa. Ukoliko se u iteraciji koristi $key => $value tada se poziva i metoda key() koja vraća vrednost za $key. Tada se pomera indeks na sledeći zapis pozivom metode next(). Nakon toga se ponovo poziva metoda valid() koja će utvrditi da li je definisan sledeći indeks. Proces se ponavlja sve dok ova metoda ne vrati vrednost false. Sada kada znamo kako se odvija proces iteracije, možemo izvršiti takozvanu “ručnu” iteraciju nad objektom klase IteratorExample, koja bi izgledala ovako:

$iterator = new IteratorExample();
$iterator->rewind();
while ($iterator->valid())
{
    $key = $iterator->key();
    $value = $iterator->current();
    echo "$key - $value";
    $iterator->next();
}

Rezultat će biti isti kao u prethodnom primeru.

Prethodni primer predstavlja najjednostavniji oblik Iterator interfejsa. Uz malu doradu naše mogućnosti u iteraciji nad objektima klasa koje implementiraju Iterator interfejs biće višestruko veće. Moći ćemo da menjamo sve navedene metode u skladu sa našim potrebama.

U sledećem primeru iskoristićemo klasu IteratorAggregateExample sa malom izmenom i doradićemo klasu IteratorExample. Klasa IteratorAggregateExample će kao ekterni iterator koristiti klasu IteratorExample. To znači da će iteracija objekta klase IteratorAggregateExample biti opisana metodama klase IteratorExample:

class IteratorAggregateExample implements IteratorAggregate
{
    protected $attributes = array('first', 'second', 'third', 'fourth');

    public function getIterator()
    {
        return new IteratorExample($this->attributes);
    }
}
class IteratorExample implements Iterator
{
    protected $attributes;
    protected $current_index = 0;

    public function __construct($attributes)
    {
        $this->attributes = $attributes;
    }

    public function rewind()
    {
        $this->current_index = 0;
    }

    public function next()
    {
        // najčeše se tekući index povećava za jedan
        // tj. linija izgleda ovako - $this->current_index++;
        $this->current_index += 2;
    }

    public function valid()
    {
        return array_key_exists($this->current_index, $this->attributes);
    }

    public function current()
    {
        return $this->attributes[$this->current_index];
    }

    public function key()
    {
        return $this->current_index;
    }
}

Kada pokrenemo kod koji vrši iteraciju:

$instance = new IteratorAggregateExample();
foreach ($instance as $variable => $value)
{
    echo ”$variable - $value 
”;      
}

kao rezultat dobijamo:

0 - first
2 - third

Rezultat je takav zato što smo u metodi next() postavili da se trenutni index povećava za dva što znači da se u iteraciji ne obrađuje svaki element nego svaki drugi. Sve metode klase IteratorExample možemo menjati i prilagoditi našim potrebama.

Ako promenimo metodu rewind() na sledeći način:

public function rewind()
{
    $this->current_index = 1;
}

i ponovo pokrenemo skriptu za iteraciju nad objektom, dobićemo sledeći rezultat:

1 - second
3 - fourth

U ovom primeru smo koristili klasu IteratorExample kao eksterni iterator klase IteratorAggregateExample. Sama logika iteracije nalazi se u klasi IteratorExample.

Primer iz prakse

Budući da smo sada upoznati sa Iterator i IteratorAggregate interfejsima, na jednostavnom primeru pokaćemo kako se koriste iteratori u praksi. Idealan primer predstavljao bi sistem za anketiranje budući da se svaka anketa sastoji iz više pitanja te vrlo lako možemo primeniti iteraciju. Primer je pojednostavljen i služi isključivo da demonstrira funkcionalnosti Iterator interfejsa. Klase sadrže samo metode za prikaz ankete i umesto metoda za dodelu vrednosti nekim promenljivim koristimo konfiguracioni niz.

class Poll implements Iterator
{
    protected $questions = array();
    protected $title;
    protected $totalVotes = 0;

    public function __construct($config)
    {
        $this->title = isset($config['title']) ? $config['title'] : 'No title';
        $this->mode =  isset($config['mode']) ? $config['mode'] : 'question';
        $this->loadQuestions($config['questionData']);
    }

    protected function loadQuestions($data)
    {
        foreach ($data as $questionData)
        {
            $this->questions[] = new Question($questionData, $this->mode);
            $this->totalVotes += $questionData['votes'];
        }
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function rewind()
    {
        reset($this->questions);
    }

    public function current()
    {
        return current($this->questions);
    }

    public function key()
    {
        return key($this->questions);
    }

    public function next()
    {
        return next($this->questions);
    }

    public function valid()
    {
        return false !== current($this->questions);
    }
}

class Question
{
    protected $questionData;
    protected $mode;
    protected $title;
    protected $votes;

    public function __construct($config, $mode)
    {
        $this->title = isset($config['title']) ? $config['title'] : 'No title';
        $this->votes = isset($config['votes']) ? $config['votes'] : 0;
        $this->mode = $mode;
    }

    public function render()
    {
        echo $this->title;
        if ($this->mode == 'answer')
        {
            echo ' - '.$this->votes;
        }
        echo '
';
    }
}

U primeru su date dve klase: Poll i Question. Anketa ima dva moda - kada prikazuje samo pitanja i kada prikazuje pitanja sa rezultatima. U našem primeru klasa Poll implementira Iterator interfejs. Na isti način smo mogli da implementiramo interfejs IteratorAggregate i koristimo neki eksterni iterator, na primer klasu ArrayIterator. Klasa Poll ima atribut $questions u koji se smešta niz objekata klase Questions. Iterator smo definisali tako da iteracijom nad objektom klase Poll zapravo prolazimo kroz članove niza promenljive $questions.

Ukoliko pokrenemo sledeći kod:

$config['title'] = 'Vase omiljeno godisnje doba je';
$config['mode'] = 'answer';
$config['questionData'][1]['title'] = 'Prolece';
$config['questionData'][1]['votes'] = 1;
$config['questionData'][2]['title'] = 'Leto';
$config['questionData'][2]['votes'] = 2;
$config['questionData'][3]['title'] = 'Jesen';
$config['questionData'][3]['votes'] = 3;
$config['questionData'][4]['title'] = 'Zima';
$config['questionData'][4]['votes'] = 4;
$poll = new Poll($config);
echo $poll->getTitle().'
';
foreach ($poll as $questionData)
{
    $questionData->render();
}

Kao rezultat dobićemo:

Vase omiljeno godisnje doba je
Prolece - 1
Leto - 2
Jesen - 3
Zima - 4

Rezultat je prikaz ankete u skladu sa prosleđenom konfiguracijom.

Možemo slobodno konstatovati da većina PHP programera razvija aplikacije koristeći MVC patern. Jedna od stvari koje programeri često rade je da, kada dobiju nekakav niz podataka, izvrše obradu tog niza u modelu (ili kontroleru), pri tome smeštajući obrađene podatke u neki drugi niz koji zatim prosleđuju na view. Tu vrše iteraciju niza i prikazuju podatke. Formiranje još jednog niza je suvišno i bespotreno trošenje memorije. Nekad se može desiti da nam rezultat takve obrade neće ni trebati u view te je sama obrada nepotrebna. Umesto toga, moguće je napraviti iterator i proslediti mu niz koji je potrebno obraditi i on će izvršiti obradu u trenutku iteracije kada je to i potrebno. Nakon toga se iterator može proslediti na view gde će se kroz iteraciju prikazati podaci. Slična greška je napravljena i u primeru koji smo naveli. Koristimo iterator, a izvršili smo iteraciju ulaznih parametara u konstruktoru što je nepotrebno i pogrešno. Da bismo ispravili grešku promenićemo konstruktor i metodu current():

public function __construct($config)
{
	$this->title = isset($config['title']) ? $config['title'] : 'No title';
	$this->mode = $config['mode'];
	! isset($config['mode']) and $config['mode'] = 'question';
	$this->questions = $config['questionData'];
}

public function current()
{
	$questionData = current($this->questions);
	$question = new Question($questionData, $this->mode);
	$this->totalVotes += $question_data['votes'];
	return $question;
}

Dalje smernice

Često nismo ni svesni koliko malo nedostaje našem kodu da bude, bolji, lepši za oko i organizovaniji. Još češće smo skloni "izmišljanju tople vode" kada pravimo nešto za šta nismo svesni da već postoji. Siguran sam da je svako od vas, odnosno nas, bar jednom ovo učinio.

Iteratori su izuzetno korisni. Njihovim korišćenjem učinićete vaš kod efikasnijim, fleksibilnijim i čistijim. Oni su višestruko primenjivi, budući da se susrećemo sa velikim brojem iteracija na svakom projektu. Pogledajte ovde koliko je klasa standardne PHP biblioteke implementiralo Iterator i IteratorAggregate interfejs. Dobro ih proučite - svaki utrošen minut višestruko će vam se isplatiti.