Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

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 in index.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!

145 lines index.php
... 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:

145 lines 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():

147 lines index.php
... 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.

Leave a comment!

7
Login or Register to join the conversation

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

1 Reply

Hey Manolis

That's interesting. Does it says why it's incompatible?

1 Reply

Btw do you know if there's a way to have interface ans class methods auto generated with intelephense ? (I'm a newbie)

Reply

Hey Manolis

I'm afraid I do not. I only work with PHPUnit (which is awesome). I'd expect there is a plugin you can install or perhaps it comes with a built-in feature

Cheers!

Reply

But i unset intelephense and put PHP tools instead and the error message wasn't there anymore

Reply

No i paste all the message

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice