Answer to the Iterator question
Tags: php, ama, showdev, coding
Published: 2018-12-04 10:59:48

---
title: Answer to the Iterator question
published: true
description: I got a question on my previous blog post about iterators, here is the answer.
tags: php, ama, showdev, coding
cover_image: https://thepracticaldev.s3.amazonaws.com/i/y5rnnun3kbac4kuwd9sk.jpg
---

I got this question
{% devcomment 7ajm %}

I found it very interesting, so I decide to give it a whole post to answer it, even there is an 80% similar code base like in the original post. I decided to approach this problem from Collection perspective since Iterator has nothing to do with the search for Id function Ryan want's to implement. Still, Iterating abilities remains same :) so everybody should be happy. Please pay strong attention to Tests I wrote, they remain the same as in the original article, just with added functionalities

The original post is here:

{% link https://dev.to/damnjan/iterator-in-php-a-practical-guide-2k59 %}

#####NOTE
If you read this question and this blog post, and you think you have better, smarter, more optimal ar cleaners solution, please feel free to contribute, I'll be happy to see other approaches. 

#####User class
User class is just an example of a class which returns Id, we gonna mock it all the time in our test, so you can implement your own
```php
// Example class with getId method
class User
{
    public function getId()
    {
        return rand(1, 1000000);
    }
}
```

#####UserIterator

Than we have unchanged Iterator
```php
class UserIterator implements \Iterator
{
    /** @var int */
    private $position = 0;

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

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

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

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

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

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

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

#####UsersCollection
And Finally Collection is here with new public methods getUserById, removeById and updateUserById. Also, note that during appending new user to the collection we also append new id to array userIds. That array contains userId as a key and an iterator position as a value. More info about how things work down in [the test section](#test_collection)
```php
class UsersCollection implements \IteratorAggregate
{
    /** @var array */
    private $users = [];

    private $userIds = [];

    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 getUserById($userId)
    {
        if (isset($this->userIds[$userId])) {
            return $this->getUser($this->userIds[$userId]);
        }

        return null;
    }

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

    public function removeById($userId)
    {
        if (isset($this->userIds[$userId])) {
            unset($this->userIds[$userId]);
        }
    }

    public function updateUserById($userId, User $user)
    {
        if ($user->getId() !== $userId) {
            throw new Exception('User Id mismatch');
        }

        if (isset($this->userIds[$userId])) {
            $position = $this->userIds[$userId];
            $this->users[$position] = $user;
        }
    }

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

    private function setUserId(User $user)
    {
        $userId = $user->getId();
        $currentPosition = count($this->users) - 1;
        $this->userIds[$userId] = $currentPosition;
    }
}
```
#####UserIteratorTest
Tests for iterator pretty much the same as in the original article, I just modified it to have actual User mock implemented.

```php
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;
    }

    /**
     * @return \PHPUnit\Framework\MockObject\MockObject | User
     */
    private function getUserMock()
    {
        $userMock = $this->getMockBuilder(User::class)->getMock();
        return $userMock;
    }
}
```
<a name="test_collection"></a>
#####UsersCollectionTest
And tests for Collection now testing for two new "get" cases, get User by existing Id and if user id does not exist return null. Also, there is the test for removing user and two cases updating an existing one.
```php
class UsersCollectionTest extends MockClass
{
    /**
     * @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());
    }

    /**
     * @covers UsersCollection
     */
    public function testSearchForUserByIdShouldReturnUserWithGivenId()
    {
        $user1 = $this->getUserMock();
        $user2 = $this->getUserMock();
        $user3 = $this->getUserMock();

        $user1->expects($this->once())
            ->method('getId')
            ->willReturn(123);
        $user2->expects($this->once())
            ->method('getId')
            ->willReturn(111);
        $user3->expects($this->once())
            ->method('getId')
            ->willReturn(345);

        $usersCollection = new UsersCollection();
        $usersCollection->addUser($user1);
        $usersCollection->addUser($user2);
        $usersCollection->addUser($user3);

        $this->assertEquals($user3, $usersCollection->getUserById(345));
        $this->assertEquals($user2, $usersCollection->getUserById(111));
        $this->assertEquals($user1, $usersCollection->getUserById(123));
    }

    /**
     * @covers UsersCollection
     */
    public function testSearchForUserByIdWhichNotExistShouldReturnNull()
    {
        $user1 = $this->getUserMock();
        $user2 = $this->getUserMock();

        $user1->expects($this->once())
            ->method('getId')
            ->willReturn(1);
        $user2->expects($this->once())
            ->method('getId')
            ->willReturn(2);

        $usersCollection = new UsersCollection();
        $usersCollection->addUser($user1);
        $usersCollection->addUser($user2);

        $this->assertEquals(null, $usersCollection->getUserById(4));
        $this->assertEquals(null, $usersCollection->getUserById(100));
    }

    /**
     * @covers UsersCollection
     */
    public funCtion testIfOneUserIsRemovedFromCollectionSearchOnUserIdShouldReturnNull()
    {
        $user1 = $this->getUserMock();
        $user2 = $this->getUserMock();
        $user3 = $this->getUserMock();

        $user1->expects($this->once())
            ->method('getId')
            ->willReturn(123);
        $user2->expects($this->once())
            ->method('getId')
            ->willReturn(111);
        $user3->expects($this->once())
            ->method('getId')
            ->willReturn(345);

        $usersCollection = new UsersCollection();
        $usersCollection->addUser($user1);
        $usersCollection->addUser($user2);
        $usersCollection->addUser($user3);

        $usersCollection->removeById(111);

        $this->assertEquals($user3, $usersCollection->getUserById(345));
        $this->assertEquals(null, $usersCollection->getUserById(111));
        $this->assertEquals($user1, $usersCollection->getUserById(123));
    }

    /**
     * @covers UsersCollection
     */
    public funCtion testUpdateUserByIdShouldReplaceUserObjectOnThisPosition()
    {
        $user1 = $this->getUserMock();
        $user2 = $this->getUserMock();
        $user3 = $this->getUserMock();
        $user4 = $this->getUserMock();
        // this property is set to ensure that object is different than $user1
        $user4->property = 4;

        $user1->expects($this->once())
            ->method('getId')
            ->willReturn(123);
        $user2->expects($this->once())
            ->method('getId')
            ->willReturn(111);
        $user3->expects($this->once())
            ->method('getId')
            ->willReturn(345);
        $user4->expects($this->once())
            ->method('getId')
            ->willReturn(123);

        $usersCollection = new UsersCollection();
        $usersCollection->addUser($user1);
        $usersCollection->addUser($user2);
        $usersCollection->addUser($user3);

        $usersCollection->updateUserById(123, $user4);

        $this->assertEquals($user4, $usersCollection->getUserById(123));
    }

    /**
     * @expectedExceptionMessage User Id mismatch
     * @expectedException \Exception
     * @covers UsersCollection
    */
    public funCtion testUpdateUserWhenUserIdAndGivenIdMismatchShouldThrowException()
    {
        $user1 = $this->getUserMock();
        $user2 = $this->getUserMock();

        $user1->expects($this->once())
            ->method('getId')
            ->willReturn(123);
        $user2->expects($this->once())
            ->method('getId')
            ->willReturn(444);

        $usersCollection = new UsersCollection();
        $usersCollection->addUser($user1);

        $usersCollection->updateUserById(123, $user2);
    }

    /**
     * @return \PHPUnit\Framework\MockObject\MockObject | User
     */
    private function getUserMock()
    {
        $userMock = $this->getMockBuilder(User::class)->getMock();
        return $userMock;
    }
}

```