IteratorAggregate: Loop over an Object!?
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
Let me show you just one other really cool, magic thing - this is my favorite.
Right now, in ShipLoader
, the getShips()
method return an array:
// ... lines 1 - 8 | |
class ShipLoader | |
{ | |
// ... lines 11 - 20 | |
public function getShips() | |
{ | |
$ships = array(); | |
$shipsData = $this->queryForShips(); | |
foreach ($shipsData as $shipData) { | |
$ships[] = $this->createShipFromData($shipData); | |
} | |
return $ships; | |
} | |
// ... lines 33 - 70 | |
} | |
Instead of doing that, I'm going to return an object - a ShipCollection
object.
Don't ask why yet. I'll show you some reasons in a minute.
Creating ShipCollection
First create a new PHP class called ShipCollection
:
// ... lines 1 - 2 | |
namespace Model; | |
class ShipCollection | |
{ | |
// ... lines 7 - 15 | |
} |
Hey, check it out: PhpStorm already correctly-guessed that this should have
the Model
namespace: it understands our PSR-0 naming convention.
Inside, add a private $ships
property: this will be an array of Ship
objects.
Then add a public function __construct()
method, give it a $ships
argument, and
set that property inside:
// ... lines 1 - 4 | |
class ShipCollection | |
{ | |
// ... lines 7 - 9 | |
private $ships; | |
public function __construct(array $ships) | |
{ | |
$this->ships = $ships; | |
} | |
} |
Above the $ships
just to help our editor with autocompletion later, add some
PHP Doc that says that this is an array of AbstractShip
:
// ... lines 1 - 4 | |
class ShipCollection | |
{ | |
/** | |
* @var AbstractShip[] | |
*/ | |
private $ships; | |
// ... lines 11 - 15 | |
} |
Obviously, ShipCollection
is a class... but its only purpose is to be a small
wrapper around an array. In ShipLoader
, instead of returning the array, return
a new ShipCollection()
object and pass it $ships
:
// ... lines 1 - 7 | |
use Model\ShipCollection; | |
class ShipLoader | |
{ | |
// ... lines 12 - 21 | |
public function getShips() | |
{ | |
// ... lines 24 - 31 | |
return new ShipCollection($ships); | |
} | |
// ... lines 34 - 71 | |
} | |
Now, stop: we're referencing ShipCollection
inside of ShipLoader
, so we need
a use
statement for it. Go to the top to add it. But wait! It's already there!
Thank you PhpStorm: it added it automatically for me when I auto-completed the class
name. Whether your editor does this or not, just make sure to not forget those
use
statements!
Finally, above the method, we're not returning an array of AbstractShip
objects
anymore: we're now returning a ShipCollection
:
// ... lines 1 - 7 | |
use Model\ShipCollection; | |
class ShipLoader | |
{ | |
// ... lines 12 - 18 | |
/** | |
* @return ShipCollection | |
*/ | |
public function getShips() | |
{ | |
// ... lines 24 - 31 | |
return new ShipCollection($ships); | |
} | |
// ... lines 34 - 71 | |
} | |
Cool Now again, don't worry about why we're doing this yet. For now, let's try to fix our app.
Implementing ArrayAccess First
First, go to index.php
. Boom!
Cannot use object of type
ShipCollection
as array inindex.php
on line 13.
No surprise. After creating the $brokenShip
, we're trying to add it to the ShipCollection
as if it were an array!
// ... lines 1 - 11 | |
$ships = $shipLoader->getShips(); | |
$brokenShip = new BrokenShip('Just a hunk of metal'); | |
$ships[] = $brokenShip; | |
// ... lines 16 - 145 |
That's not allowed... oh wait it is! Open ShipCollection
and make it implement \ArrayAccess
:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess | |
{ | |
// ... lines 7 - 35 | |
} |
Now, at the bottom, I'll open the "Code"->"Generate" menu and implement the same 4 methods
as before. This is even easier now: in offsetExists()
, use
array_key_exists($offset, $this->ships)
. The other methods are even easier: I'll
fill each in by acting on the $ships
array property:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess | |
{ | |
// ... lines 7 - 16 | |
public function offsetExists($offset) | |
{ | |
return array_key_exists($offset, $this->ships); | |
} | |
public function offsetGet($offset) | |
{ | |
return $this->ships[$offset]; | |
} | |
public function offsetSet($offset, $value) | |
{ | |
$this->ships[$offset] = $value; | |
} | |
public function offsetUnset($offset) | |
{ | |
unset($this->ships[$offset]); | |
} | |
} |
Perfect! The ShipCollection
object can now act like an array.
So refresh again! It works!
You can't Loop Over an Object :(
Ok, let's start a battle. Woh: check this out - there are no ships. What's going on here?
Look back at index.php
:
// ... lines 1 - 36 | |
<html> | |
// ... lines 38 - 62 | |
<body> | |
<div class="container"> | |
<div class="page-header"> | |
<h1>OO Battleships of Space</h1> | |
</div> | |
<table class="table table-hover"> | |
// ... lines 69 - 79 | |
<tbody> | |
<?php foreach ($ships as $ship): ?> | |
// ... lines 82 - 95 | |
<?php endforeach; ?> | |
</tbody> | |
</table> | |
// ... lines 99 - 141 | |
</div> | |
</body> | |
</html> |
Eventually we try to loop over the $ships
variable but this is a ShipCollection
object!
It turns out that after implementing ArrayAccess
, we can use the array syntax with an object,
but we still cannot loop over it like an array.
The IteratorAggregate Interface
Can we teach PHP how to loop over our object? Absolutely: and the answer is
another interface. To implement a second interface, add a comma and then use
\IteratorAggregate
:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess, \IteratorAggregate | |
// ... lines 6 - 42 |
Repeat our trick from before: "Code"->"Generate" and then "Implement Methods". This time
we only need to add one method: getIterator()
. The easiest way to make this work
is to return another core helper class: return new \ArrayIterator()
and pass that
$this->ships
:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess, \IteratorAggregate | |
{ | |
// ... lines 7 - 36 | |
public function getIterator() | |
{ | |
return new \ArrayIterator($this->ships); | |
} | |
} |
This tells PHP that when we try to loop over this object, it should actually loop
over the $ships
array property.
Ok, give it a try. Hey guys, we have ships! By adding 2 interfaces, we've made our ShipCollection object look and act almost exactly like an array.
Why did we Do this?
Ok, let's finally answer the question: why did we do this? Because sometimes, it might be useful to add some helpful methods to an array. Well, of course you can't do that, but you can add methods to a class.
For example, add a new method called public function removeAllBrokenShips()
,
because maybe we want a collection of only working ships. By adding this method,
that would be really easy:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess, \IteratorAggregate | |
{ | |
// ... lines 7 - 41 | |
public function removeAllBrokenShips() | |
{ | |
// ... lines 44 - 48 | |
} | |
} |
Inside, loop over $this->ships as $key => $ship
. Then, if !$ship->isFunctional()
,
unset($this->ships[$key])
:
// ... lines 1 - 4 | |
class ShipCollection implements \ArrayAccess, \IteratorAggregate | |
{ | |
// ... lines 7 - 41 | |
public function removeAllBrokenShips() | |
{ | |
foreach ($this->ships as $key => $ship) { | |
if (!$ship->isFunctional()) { | |
unset($this->ships[$key]); | |
} | |
} | |
} | |
} |
Let's test this fancy new method out. In index.php
, call $ships->removeAllBrokenShips()
:
// ... lines 1 - 11 | |
$ships = $shipLoader->getShips(); | |
$brokenShip = new BrokenShip('Just a hunk of metal'); | |
$ships[] = $brokenShip; | |
// ... lines 16 - 18 | |
$ships->removeAllBrokenShips(); | |
// ... lines 20 - 147 |
This looks and acts like an array, but with the super-power to have methods on it. ooOOOooo.
Refresh and check this out: no more broken ships, ever.
There are more of these interfaces that have special powers, but these are the most common ones. And the most important thing is just to understand that they exist and how they work.
Returning a new \ArrayIterator seems not being ok. I get this message from intelephense in vs code (Method 'Model\ShipCollection::getIterator()' is not compatible with method 'IteratorAggregate::getIterator()')
However it works, so the problem must come from vs code i guess