Give the Traversable Interface Some Love
- Posted on
- Categories: Best Practices
- Comments
Every once in a while, I get into an argument about extending PHP's
Traversable
interface instead of the Iterator interface in custom interfaces.
The confusion seems to result from the official documentation of Traversable
.
Let me clarify why you should give Traversable
much more love than you do
right now.
Let's start with a simple use case. Let's create an interface ArrayInterface
that demarcates objects that behave like PHP arrays. The interface should allow
for counting, iterating and array access. See this short example for a code
using this interface:
$arrayObject = /* some object implementing ArrayInterface */;
echo count($arrayObject) . "\n";
echo $arrayObject[count($arrayObject) - 1] . "\n";
foreach ($arrayObject as $key => $value) {
echo $key . ': ' . $value . "\n";
}
$arrayCopy = iterator_to_array($arrayObject);
How would you implement ArrayInterface
? The common solution is to choose among
the following two options:
Extending Iterator
interface ArrayInterface extends \Countable, \ArrayAccess, \Iterator;
Extending IteratorAggregate
interface ArrayInterface extends \Countable, \ArrayAccess, \IteratorAggregate;
In the first case, implementations of ArrayInterface
must provide the methods
current()
, key()
, next()
, rewind()
and valid()
. In the second case,
they must implement a simple getIterator()
method returning an iterator. All
of these methods are required by the PHP engine to know how to do the actual
iteration. But you, the user ArrayInterface, will never use them. You simply
want to iterate (at least in 99% of the cases; for the other 1% this post does
not apply).
Let the Traversable
interface come to rescue. From its documentation:
Interface to detect if a class is traversable using foreach.
Sounds perfect. Let's read on:
This is an internal engine interface which cannot be implemented in PHP scripts. Either IteratorAggregate or Iterator must be used instead. When implementing an interface which extends Traversable, make sure to list IteratorAggregate or Iterator before its name in the implements clause.
And this seems to be the source of confusion. Don't misread this paragraph as
"this interface is internal, please don't use it". It is not! What the paragraph
states is that classes cannot implement this interface without also
implementing IteratorAggregate
or Iterator
at the same time. As stated in
the last sentence, it is very valid to extend the interface in your own
interfaces to leave the choice of the concrete iterator implementation to the
implementing class.
With all this knowledge in mind, let's finally implement ArrayInterface
:
interface ArrayInterface extends \Countable, \ArrayAccess, \Traversable;
Implementing classes can now freely choose to implement either Iterator
or
IteratorAggregate
:
class ArrayImpl1 implements \Iterator, ArrayInterface
{
public function current() { ... }
public function key() { ... }
public function next() { ... }
public function rewind() { ... }
public function valid() { ... }
// ...
}
class ArrayImpl2 implements \IteratorAggregate, ArrayInterface
{
public function getIterator() { ... }
// ...
}