1.4. Iterator

1.4.1. Intent

According to the Gang of Four, the Iterator pattern aims to “provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 257).

1.4.2. When to use it?

The Iterator pattern should be used in various cases:

  • you need to have access to an element of a collection without exposing the internal structure of that collection
  • your application needs to support multiple simultaneous traversals of a collection.
  • you want to provide a uniform interface for traversing different types of collections

1.4.3. Diagram

Created using PhpStorm and yFiles.

1.4.3.1. Using Standard PHP Library (SPL)

Class diagram of the Iterator pattern (using SPL)

1.4.3.2. Without Standard PHP Library (SPL)

Class diagram of the Iterator pattern (without SPL)

1.4.4. Implementation

Bottle.php

 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
<?php

namespace Phpatterns\Behavioral\Iterator;

class Bottle
{
    /** @var string */
    private $name;

    /** @var string */
    private $type;

    public function __construct($name, $type)
    {
        $this->name = $name;
        $this->type = $type;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return "$this->name - $this->type";
    }
}

BottleCrate.php

 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
<?php

namespace Phpatterns\Behavioral\Iterator;

class BottleCrate
{
    /** @var Bottle[] */
    private $bottles;

    public function __construct()
    {
        $this->bottles = [];
    }
    public function getCount()
    {
        return count($this->bottles);
    }

    /**
     * @param int $bottleNumber
     * @return null|Bottle
     */
    public function getBottle($bottleNumber)
    {
        if ($bottleNumber < $this->getCount()) {
            return $this->bottles[$bottleNumber];
        }

        return null;
    }

    /**
     * @param Bottle $bottle
     */
    public function addBottle(Bottle $bottle)
    {
        if (!in_array($bottle, $this->bottles, true)) {
            $this->bottles[] = $bottle;
        }
    }

    /**
     * @param Bottle $bottle
     */
    public function removeBottle(Bottle $bottle)
    {
        $key = array_search($bottle, $this->bottles, true);
        if ($key !== false) {
            unset($this->bottles[$key]);
        }
    }
}

1.4.4.1. Using Standard PHP Library (SPL)

BottleCrateIterator.php

 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
<?php

namespace Phpatterns\Behavioral\Iterator\UsingSPL;

use Phpatterns\Behavioral\Iterator;

/**
 * We are using the Iterator interface provided by PHP
 * @link http://php.net/manual/en/class.iterator.php
 */
class BottleCrateIterator implements \Iterator
{
    /** @var Iterator\BottleCrate */
    private $bottleCrate;

    /** @var int */
    private $index;

    public function __construct(Iterator\BottleCrate $bottleCrate)
    {
        $this->bottleCrate = $bottleCrate;
        $this->index = 0;
    }

    /**
     * Return the current Bottle
     * @return Iterator\Bottle
     */
    public function current()
    {
        return $this->bottleCrate->getBottle($this->index);
    }

    /**
     * Move forward to next Bottle
     */
    public function next()
    {
        $this->index++;
    }

    /**
     * Return the key of the current Bottle
     * @return int
     */
    public function key()
    {
        return $this->index;
    }

    /**
     * Checks if current position is valid
     * @return bool
     */
    public function valid()
    {
        return null !== $this->bottleCrate->getBottle($this->index);
    }

    /**
     * Rewind the Iterator to the first Bottle
     */
    public function rewind()
    {
        $this->index = 0;
    }
}

1.4.4.2. Without Standard PHP Library (SPL)

IteratorInterface.php

 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
<?php

namespace Phpatterns\Behavioral\Iterator\WithoutSPL;

interface IteratorInterface
{
    /**
     * Rewind the Iterator to the first element
     */
    public function currentItem();

    /**
     * Return the current element
     */
    public function first();

    /**
     * Return the key of the current element
     */
    public function isDone();

    /**
     * Move forward to next element
     */
    public function next();
}

BottleCrateIterator.php

 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
<?php

namespace Phpatterns\Behavioral\Iterator\WithoutSPL;

use Phpatterns\Behavioral\Iterator;

class BottleCrateIterator implements IteratorInterface
{
    /** @var Iterator\BottleCrate */
    private $bottleCrate;

    /** @var int */
    private $index;

    public function __construct(Iterator\BottleCrate $bottleCrate)
    {
        $this->bottleCrate = $bottleCrate;
    }

    /**
     * @return Iterator\Bottle
     */
    public function currentItem()
    {
        if (!$this->isDone()) {
            return $this->bottleCrate->getBottle($this->index);
        }

        throw new \OutOfBoundsException("No more bottle in the crate.");
    }

    public function first()
    {
        $this->index = 0;
    }

    public function isDone()
    {
        return $this->index >= $this->bottleCrate->getCount();
    }

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

1.4.5. Tests

BottleCrateTest.php

 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
<?php

namespace Test\Phpatterns\Behavioral\Iterator;

use Phpatterns\Behavioral\Iterator;

class BottleCrateTest extends \PHPUnit_Framework_TestCase
{
    /** @var Iterator\BottleCrate */
    private $bottleCrate;

    protected function setUp()
    {
        $this->bottleCrate = new Iterator\BottleCrate();
        $this->bottleCrate->addBottle(new Iterator\Bottle('Coca Cola', 'Soda'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Château Rayas', 'Wine'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Dom Pérignon', 'Champagne'));
    }

    public function testCount()
    {
        $this->assertEquals(3, $this->bottleCrate->getCount());
    }

    public function testAddBottleToTheCrate()
    {
        $this->bottleCrate->addBottle(new Iterator\Bottle('Coca Cola', 'Soda'));
        $this->assertEquals(4, $this->bottleCrate->getCount());
    }

    public function testRemoveExistingBottleFromTheCrate()
    {
        $this->bottleCrate->removeBottle($this->bottleCrate->getBottle(1));
        $this->assertEquals(2, $this->bottleCrate->getCount());
    }

    public function testRemoveNonExistingBottleFromTheCrate()
    {
        $this->bottleCrate->removeBottle(new Iterator\Bottle('Coca Cola', 'Soda'));
        $this->assertEquals(3, $this->bottleCrate->getCount());
    }

    public function testGetBottle()
    {
        $this->assertEquals(
            'Château Rayas - Wine',
            $this->bottleCrate->getBottle(1)
        );
    }
}

1.4.5.1. Using Standard PHP Library (SPL)

IteratorTest.php

 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
<?php

namespace Test\Phpatterns\Behavioral\Iterator\UsingSPL;

use Phpatterns\Behavioral\Iterator;
use Phpatterns\Behavioral\Iterator\UsingSPL;

class IteratorTest extends \PHPUnit_Framework_TestCase
{
    /** @var Iterator\BottleCrate */
    private $bottleCrate;

    protected function setUp()
    {
        $this->bottleCrate = new Iterator\BottleCrate();
        $this->bottleCrate->addBottle(new Iterator\Bottle('Coca Cola', 'Soda'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Château Rayas', 'Wine'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Dom Pérignon', 'Champagne'));
    }

    /**
     * Testing the iterator mechanism
     */
    public function testIteratorMechanism()
    {
        $bottleDescriptions = ['Coca Cola - Soda', 'Château Rayas - Wine', 'Dom Pérignon - Champagne'];
        $bottleCrateIterator = new UsingSPL\BottleCrateIterator($this->bottleCrate);

        while ($bottleCrateIterator->valid()) {
            $this->assertSame(
                $bottleDescriptions[$bottleCrateIterator->key()],
                $bottleCrateIterator->current()->__toString()
            );
            $bottleCrateIterator->next();
        }

        $this->assertEquals(3, $bottleCrateIterator->key());
        $bottleCrateIterator->rewind();
        $this->assertEquals(0, $bottleCrateIterator->key());
    }
}

1.4.5.2. Without Standard PHP Library (SPL)

IteratorTest.php

 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
<?php

namespace Test\Phpatterns\Behavioral\Iterator\WithoutSPL;

use Phpatterns\Behavioral\Iterator;
use Phpatterns\Behavioral\Iterator\WithoutSPL;

class IteratorTest extends \PHPUnit_Framework_TestCase
{
    /** @var Iterator\BottleCrate */
    private $bottleCrate;

    protected function setUp()
    {
        $this->bottleCrate = new Iterator\BottleCrate();
        $this->bottleCrate->addBottle(new Iterator\Bottle('Coca Cola', 'Soda'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Château Rayas', 'Wine'));
        $this->bottleCrate->addBottle(new Iterator\Bottle('Dom Pérignon', 'Champagne'));
    }

    /**
     * Testing the iterator mechanism
     */
    public function testIteratorMechanism()
    {
        $bottleDescriptions = ['Coca Cola - Soda', 'Château Rayas - Wine', 'Dom Pérignon - Champagne'];
        $bottleCrateIterator = new WithoutSPL\BottleCrateIterator($this->bottleCrate);

        for ($bottleCrateIterator->first(); !$bottleCrateIterator->isDone(); $bottleCrateIterator->next()) {
            $this->assertSame(
                array_shift($bottleDescriptions),
                $bottleCrateIterator->currentItem()->__toString()
            );
        }

        $this->setExpectedException('OutOfBoundsException', 'No more bottle in the crate.');
        $bottleCrateIterator->currentItem();
    }
}