Oggi propongo un semplice esercizio in PHP dove vogliamo costruire un oggetto iterabile che sia accessibile come un vettore e percorribile da foreach.
Immaginiamo di avere due classi Studente e Scuola, quello che vogliamo ottenere è il qualcosa di analogo a questo:
1 2 3 4 5 6 7 8 9 10 11 |
$scuola = new Scuola(); $scuola->add(new Studente("Squall")); $scuola->add(new Studente("Zell")); $scuola->add(new Studente("Rinoa")); $scuola[] = new Studente("Edea"); echo "in tutto ci sono " . count($scuola) . " studenti<br>"; foreach( $scuola as $k => $v ) { echo $k . " -> " . $v->getNome() . "<br>"; } |
L’output sarà così:
in tutto ci sono 4 studenti
0 -> Squall
1 -> Zell
2 -> Rinoa
3 -> Edea
Anzitutto cominciamo costruendo la classe Studente. Questa è molto semplice e non ha nulla di particolare, la possiamo costruire come vogliamo ed in particolare, ai fini dell’esercizio, la farò con una sola proprietà e due metodi (set e get).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Studente { private $nome; public function __construct( $nome ) { $this->nome = $nome; } public function getNome() { return $this->nome; } public function setNome( $nome ) { $this->nome = $nome; } } |
Adesso costruiamo Scuola implementando i metodi Iterator, ArrayAccess, Countable.
Le tre interfacce permettono di implementare diverse proprietà. Affinché l’oggetto costruito con Scuola sia percorribile dal foreach usiamo l’interfaccia Iterator.
L’interfaccia implementa i seguenti metodi astratti:
1 2 3 4 5 6 7 8 |
Iterator extends Traversable { /* Methods */ abstract public current ( void ) : mixed abstract public key ( void ) : scalar abstract public next ( void ) : void abstract public rewind ( void ) : void abstract public valid ( void ) : bool } |
Si suppone che la nostra classe abbia una proprietà “posizione“, per cui si possa ricavare un valore da questa posizione corrente con current(). Il metodo key() dovrà restituire tale posizione corrente, next() permetterà di andare alla posizione successiva, mentre rewind() consentirà di ripartire dalla posizione iniziale. Infine il metodo valid() verificherà se nella posizione attuale esiste o meno un oggetto.
Cominciamo allora costruendo la classe nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Scuola implements Iterator { private $position = 0; private $studenti = array(); public function __construct() { $this->position = 0; } /* metodi Iterator */ public function rewind() { $this->position = 0; } public function current() { return $this->studenti[$this->position]; } public function key() { return $this->position; } public function next() { ++$this->position; } public function valid() { return isset($this->studenti[$this->position]); } } |
Per rendere la classe operativa aggiungiamo anche un altro paio di metodi. In particolare:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Scuola implements Iterator { /* metodi precedenti ... */ /* metodi Scuola */ public function add( $studente ) { if( $studente instanceof Studente ) $this->studenti[] = $studente; else $this->erroreOggettoNonValido(); } /* errori */ private function erroreOggettoNonValido() { throw new InvalidArgumentException("l'oggetto deve essere di tipo Studente"); } } |
Con il controllo if( $studente instanceof Studente )
vogliamo assicurarci che l’oggetto aggiunto tramite il metodo add()
sia del tipo Studente
. Se il tipo non è quello giusto solleviamo un errore del tipo InvalidArgumentException
dove avvisiamo l’utente.
Significa che la forma:
1 2 |
$scuola = new Scuola(); $scuola->add("Squall"); |
Produrrà un errore del tipo Fatal error: Uncaught InvalidArgumentException: l'oggetto deve essere di tipo Studente in iterator.php:92 Stack trace: #0 iterator.php(87): Scuola->erroreOggettoNonValido() #1 iterator.php(98): Scuola->add('Squall') #2 {main} thrown in iterator.php on line 92
L’operazione valida sarà invece:
1 2 |
$scuola = new Scuola(); $scuola->add(new Studente("Squall")); |
A questo punto l’oggetto è già iterabile mediante foreach, che può essere chiamato nel modo seguente:
1 2 3 |
foreach( $scuola as $k => $v ) { echo $k . " -> " . $v->getNome() . "<br>"; } |
Vogliamo dare la possibilità di aggiungere gli oggetti anche alla maniera di $scuola[] = new Studente("Edea");
Per farlo implementiamo la nostra classe Scuola anche su ArrayAccess. L’interfaccia ArrayAccess richiede i seguenti metodi:
1 2 3 4 5 6 7 |
ArrayAccess { /* Methods */ abstract public offsetExists ( mixed $offset ) : bool abstract public offsetGet ( mixed $offset ) : mixed abstract public offsetSet ( mixed $offset , mixed $value ) : void abstract public offsetUnset ( mixed $offset ) : void } |
Quindi implementiamoli nella nostra classe Scuola alla maniera seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Scuola implements Iterator, ArrayAccess { /* inizializzazione ... */ /* metodi Iterator */ /* metodi ArrayAccess */ public function offsetSet($offset, $value) { if( $value instanceof Studente ) { if (is_null($offset)) { $this->studenti[] = $value; } else { $this->studenti[$offset] = $value; } } else { $this->erroreOggettoNonValido(); } } public function offsetExists($offset) { return isset($this->studenti[$offset]); } public function offsetUnset($offset) { unset($this->studenti[$offset]); } public function offsetGet($offset) { return $this->studenti[$offset] ?? null; } /* metodi Scuola */ } |
Anche qui usiamo if( $value instanceof Studente )
per verificare che l’oggetto sia di tipo Studente
. Nel metodo offsetGet()
utilizziamo il null coalescing operator per restituire null nel caso in cui l’oggetto non esista.
A questo punto l’istruzione $scuola[] = new Studente("Edea");
diventa utilizzabile, mentre $scuola[] = "Edea";
produce il solito errore Fatal error: Uncaught InvalidArgumentException: l'oggetto deve essere di tipo Studente in...
Infine vogliamo poter usare il metodo count($scuola)
sul nostro oggetto, ottenendo il conteggio di tutti gli studenti inseriti. Se lo usiamo senza ulteriori implementazioni il risultato sarebbe sempre e comunque 1.
L’interfaccia Countable richiede un unico metodo:
1 2 3 4 |
Countable { /* Methods */ abstract public count ( void ) : int } |
Implementiamolo nel modo seguente:
1 2 3 4 |
/* metodi Countable */ public function count() { return count($this->studenti); } |
Probabilmente sarebbe stato più carino implementare un metodo interno alla classe, magari un contatore che tenesse conto delle aggiunte, ma ai fini dell’esercizio ci accontentiamo di chiamare count
sul vettore interno degli studenti.
Fatto tutto questo vediamo il risultato finale così ottenuto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
class Studente { private $nome; public function __construct( $nome ) { $this->nome = $nome; } public function getNome() { return $this->nome; } public function setNome( $nome ) { $this->nome = $nome; } } class Scuola implements Iterator, ArrayAccess, Countable { private $position = 0; private $studenti = array(); public function __construct() { $this->position = 0; } /* metodi Iterator */ public function rewind() { $this->position = 0; } public function current() { return $this->studenti[$this->position]; } public function key() { return $this->position; } public function next() { ++$this->position; } public function valid() { return isset($this->studenti[$this->position]); } /* metodi ArrayAccess */ public function offsetSet($offset, $value) { if( $value instanceof Studente ) { if (is_null($offset)) { $this->studenti[] = $value; } else { $this->studenti[$offset] = $value; } } else { $this->erroreOggettoNonValido(); } } public function offsetExists($offset) { return isset($this->studenti[$offset]); } public function offsetUnset($offset) { unset($this->studenti[$offset]); } public function offsetGet($offset) { return $this->studenti[$offset] ?? null; } /* metodi Countable */ public function count() { return count($this->studenti); } /* metodi Scuola */ public function add( $studente ) { if( $studente instanceof Studente ) $this->studenti[] = $studente; else $this->erroreOggettoNonValido(); } /* errori */ private function erroreOggettoNonValido() { throw new InvalidArgumentException("l'oggetto deve essere di tipo Studente"); } } $scuola = new Scuola(); $scuola->add(new Studente("Squall")); $scuola->add(new Studente("Zell")); $scuola->add(new Studente("Rinoa")); $scuola[] = new Studente("Edea"); echo "in tutto ci sono " . count($scuola) . " studenti<br>"; foreach( $scuola as $k => $v ) { echo $k . " -> " . $v->getNome() . "<br>"; } |