DEV Community

Cover image for Iterator in PHP (A practical guide)
Damnjan Jovanovic
Damnjan Jovanovic

Posted on • Edited on

Iterator in PHP (A practical guide)

Every time I see this

$users = [new User(), new User()];
Enter fullscreen mode Exit fullscreen mode

I see a lost opportunity to use Iterator.

Why Iterators?

Collections are an awesome way to organize your previously no-named array. There is a couple of reasons why you should use iterators. One of reason stays for behavior, you can specify exact behavior on standard calls such as next, current, valid etc. Other reason could be that you want to ensure that collection contains an only specific type of an object.

Understand a suffer from using an array of unknown value types.
Very common in the PHP world arrays are used to store all kind of data, in many dimensions in many nested forms. Arrays introduced infinite flexibility to the developer, but because of that, they become very evil.

Example:

  • Your function (getUsers) returns an array of User objects.
  • Another function (setUsersToActiveState) using getUsers output array and set all users active status to true.
  • setUsersToActiveState loop through the array and expect to call a specific method on array item. For example, the method name is getActiveStatus.
  • If given array is an array of desired objects which have a callable method getActiveStatus, all fine. But if not exception will be thrown.
  • How we can ensure that given array is always an array of objects of a specific type?
public function getUsers(): array
{
    /** 
    here happen something which gets users from database
    ....
    **/
    return $userArray;
}

public function setUsersToActiveState()
{
    $users = $this->getUsers();
     /** @var User $param */
    foreach ($users as $user) {
        if(!$user->getActiveStatus()) {
            $user->setActiveStatus(true);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

There immediately two problems occurred.

  1. One is the problem of type. Our IDE doesn't know what's inside array of $users, so because of that IDE can't suggest us how to use $user element. (I put this comment block /** @var User $param */ above foreach, it works for phpStorm and I guess for some other IDEs)
  2. Your colleagues! How they possibly know what's inside array if there is no any hint.
  3. Bonus problem, getUsers can return literally any array and there won't be warning in the system.

Solution

// Create a collection which accepts only Users 

class UsersCollection implements \IteratorAggregate
{
    /** @var array */
    private $users = [];

    public function getIterator() : UserIterator
    {
        return new UserIterator($this);
    }

    public function getUser($position)
    {
        if (isset($this->users[$position])) {
            return $this->users[$position];
        }

        return null;
    }

    public function count() : int
    {
        return count($this->users);
    }

    public function addUser(User $users)
    {
        $this->users[] = $users;
    }
}

// Create an Iterator for User
class UserIterator implements \Iterator
{
    /** @var int */
    private $position = 0;

    /** @var UsersCollection */
    private $userCollection;

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

    public function current() : User
    {
        return $this->userCollection->getUser($this->position);
    }

    public function next()
    {
        $this->position++;
    }

    public function key() : int
    {
        return $this->position;
    }

    public function valid() : bool
    {
        return !is_null($this->userCollection->getUser($this->position));
    }

    public function rewind()
    {
        $this->position = 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Tests

Off course there is the tests to ensure that our Collection and Iterator works like a charm. For this example I using syntax for PHPUnit framework.

class UsersCollectionTest extends TestCase
{
    /**
     * @covers UsersCollection
     */
    public function testUsersCollectionShouldReturnNullForNotExistingUserPosition()
    {
        $usersCollection = new UsersCollection();

        $this->assertEquals(null, $usersCollection->getUser(1));
    }

    /**
     * @covers UsersCollection
     */
    public function testEmptyUsersCollection()
    {
        $usersCollection = new UsersCollection();

        $this->assertEquals(new UserIterator($usersCollection), $usersCollection->getIterator());

        $this->assertEquals(0, $usersCollection->count());
    }

    /**
     * @covers UsersCollection
     */
    public function testUsersCollectionWithUserElements()
    {
        $usersCollection = new UsersCollection();
        $usersCollection->addUser($this->getUserMock());
        $usersCollection->addUser($this->getUserMock());

        $this->assertEquals(new UserIterator($usersCollection), $usersCollection->getIterator());
        $this->assertEquals($this->getUserMock(), $usersCollection->getUser(1));
        $this->assertEquals(2, $usersCollection->count());
    }

    private function getUserMock()
    {
        // returns the mock of User class
    }
}


class UserIteratorTest extends MockClass
{
    /**
     * @covers UserIterator
     */
    public function testCurrent()
    {
        $iterator = $this->getIterator();
        $current = $iterator->current();

        $this->assertEquals($this->getUserMock(), $current);
    }

    /**
     * @covers UserIterator
     */
    public function testNext()
    {
        $iterator = $this->getIterator();
        $iterator->next();

        $this->assertEquals(1, $iterator->key());
    }

    /**
     * @covers UserIterator
     */
    public function testKey()
    {
        $iterator = $this->getIterator();

        $iterator->next();
        $iterator->next();

        $this->assertEquals(2, $iterator->key());
    }

    /**
     * @covers UserIterator
     */
    public function testValidIfItemInvalid()
    {
        $iterator = $this->getIterator();

        $iterator->next();
        $iterator->next();
        $iterator->next();

        $this->assertEquals(false, $iterator->valid());
    }

    /**
     * @covers UserIterator
     */
    public function testValidIfItemIsValid()
    {
        $iterator = $this->getIterator();

        $iterator->next();

        $this->assertEquals(true, $iterator->valid());
    }

    /**
     * @covers UserIterator
     */
    public function testRewind()
    {
        $iterator = $this->getIterator();

        $iterator->rewind();

        $this->assertEquals(0, $iterator->key());
    }

    private function getIterator() : UserIterator
    {
        return new UserIterator($this->getCollection());
    }

    private function getCollection() : UsersCollection
    {
        $userItems[] = $this->getUserMock();
        $userItems[] = $this->getUserMock();

        $usersCollection = new UsersCollection();

        foreach ($userItems as $user) {
            $usersCollection->addUser($user);
        }

        return $usersCollection;
    }

    private function getUserMock()
    {
        // returns the mock of User class
    }
}

Enter fullscreen mode Exit fullscreen mode

Usage

public function getUsers(): UsersCollection
{
    $userCollection = new UsersCollection();
    /** 
    here happen something which gets users from database
    ....
    **/
    foreach ($whatIGetFromDatabase as $user) {
        $userCollection->addUser($user);
    }
    return $userCollection;
}

public fucntion setUsersToActiveState()
{
    $users = $this->getUsers();

    foreach ($users as $user) {
        if(!$user->getActiveStatus()) {
            $user->setActiveStatus(true);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see setUsersToActiveState remains the same, we only do not need to specify for our IDE or collagues what type $users variable is.

Extending functionalities

Believe or not you can reuse this two objects and just change names of variables to fit most of the needs. But if you want any more complex functionality, than feel free to add it in iterator or collection.

Example 1

For example, let's say that userCollection accepts only users with age more than 18. Implementation will happen in UsersCollection class in the method addUser.

    public function addUser(User $users)
    {
        if ($user->getAge() > 18) {
            $this->users[] = $users;
        }    
    }

Enter fullscreen mode Exit fullscreen mode

Example 2

You need to add bulk users. Then you can expand your userCollection with additional method addUsers and it might look like this.


    public function addUsers(array $users)
    {
        foreach($users as $user) {
            $this->addUser(User $users);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Note

I found this great article which answers, why is generally a bad idea to return an array, and I can't agree more with @aleksikauppila on this topic

Top comments (4)

Collapse
 
godsgood33 profile image
Ryan P • Edited

How would you implement a solution where the collection indexes didn't start at 0? In the project I'm working on, there are several places where traversal of a large array takes too long, so I specify the index with a known number. Then I can do a isset($arr[$obj->id]) to find, retrieve, delete, or overwrite the object at that position. I tried to implement this adapting your code but it creates a problem with the $position variable in the iterator. In PHP 7.3 they added a array_key_first() method to more easily get the first key, but I'm operating in 7.2. I've tried a foreach hack, but it creates an internal loop in the iterator.

Collapse
 
damnjan profile image
Damnjan Jovanovic

Hei Ryan,
I found your question interesting, so I decided to answer in a longer form with a whole blog post. Please let me know did I answer your question and let me know if I could make it better.
Cheers!

Collapse
 
lishaak profile image
Lishaak

there is a typo in: public fucntion setUsersToActiveState
fucntion->function :D

Collapse
 
damnjan profile image
Damnjan Jovanovic

I fixed the mistake, thank you so much for pointing out :) You have a sharp eye