Question

Am I correct in thinking that IteratorAggregate only provides array-like read access to an object? If I need to write to the object-as-array, then I need to use Iterator?

Demonstration of an IteratorAggregate object causing a fatal error when trying to add a new element follows:

<?

class foo implements IteratorAggregate {

    public $_array = array('foo'=>'bar', 'baz'=>'quux');

    public function getIterator() {
        return new ArrayIterator($this->_array);
    }

} // end class declaration


$objFoo = & new foo();

foreach ( $objFoo as $key => $value ) {
        echo "key: $key, value: $value\n";
}

$objFoo['reeb'] = "roob";

foreach ( $objFoo as $key => $value ) {
        echo "key: $key, value: $value\n";
}

$ php test.php
key: foo, value: bar
key: baz, value: quux

Fatal error: Cannot use object of type foo as array in /var/www/test.php on line 20

Call Stack:
    0.0009      57956   1. {main}() /var/www/test.php:0
Was it helpful?

Solution

For what it's worth, given your sample class, you could have just made use of the ArrayObject (which implements IteratorAggregate and ArrayAccess like we want).

class Foo extends ArrayObject
{
    public function __construct()
    {
        parent::__construct(array('foo' => 'bar', 'baz' => 'quux'));
    }
}

$objFoo = new Foo();

foreach ( $objFoo as $key => $value ) {
    echo "$key => $value\n";
}

echo PHP_EOL;

$objFoo['foo'] = 'badger';
$objFoo['baz'] = 'badger';
$objFoo['bar'] = 'badger';

foreach ( $objFoo as $key => $value ) {
    echo "$key => $value\n";
}

With the output being, as expected:

foo => bar
baz => quux

foo => badger
baz => badger
bar => badger

OTHER TIPS

Ok, now that I better understand what you mean, let me try to explain here.

Implementing an iterator (either via the Iterator or IteratorAggregate interface) will only add functionality when iterating. What that means, is it only affects the ability to use foreach. It doesn't change or alter any other use of the object. Neither of which directly support "writing" to the iterator. I say directly, since you can still write to the base object while iterating, but not inside of the foreach.

That means that:

foreach ($it as $value) {
    $it->foo = $value;
}

Will work fine (since it's writing to the original object).

While

foreach ($it as &$value) {
    $value = 'bar';
}

Most likely won't work since it's trying to write through the iterator (which isn't directly supported. It may be able to be done, but it's not the core design).

To see why, let's look at what's going on behind the scenes with that foreach ($obj as $value). It basically is identical to:

For Iterator classes:

$obj->rewind();
while ($obj->valid()) {
    $value = $obj->current();
    // Inside of the loop here
    $obj->next();
}

For IteratorAggregate classes:

$it = $obj->getIterator();
$it->rewind();
while ($it->valid()) {
    $value = $it->current();
    // Inside of the loop here
    $it->next();
}

Now, do you see why writing isn't supported? $iterator->current() does not return a reference. So you can't write to it directly using foreach ($it as &$value).

Now, if you wanted to do that, you'd need to alter the iterator to return a reference from current(). However, that would break the interface (since it would change the methods signature). So that's not possible. So that means that writing to the iterator is not possible.

However, realize that it only affects the iteration itself. It has nothing to do with accessing the object from any other context or in any other manor. $obj->bar will be exactly the same for an iterator object as it is for a non iterator object.

Now, there comes a difference between the Iterator and IteratorAggregate. Look back at the while equivalencies, and you may be able to see the difference. Suppose we did this:

foreach ($objFoo as $value) {
    $objFoo->_array = array();
    print $value . ' - ';
}

What would happen? With an Iterator, it would only print the first value. That's because the iterator operates directly on the object for each iteration. And since you changed what the object iterates upon (the internal array), the iteration changes.

Now, if $objFoo is an IteratorAggregate, it would print all the values that existed at the start of iteration. That's because you made a copy of the array when you returned the new ArrayIterator($this->_array);. So you can continue to iterate over the entire object as it was at the start of iteration.

Notice that key difference. Each iteration of a Iterator will be dependent upon the state of the object at that point in time. Each iteration of a IteratorAggregate will be dependent upon the state of the object at the start of iteration.* Does that make sense?

Now, as for your specific error. It has nothing to do with iterators at all. You're trying to access (write actually) a variable outside of the iteration. So it doesn't involve the iterator at all (with the exception that you're trying to check the results by iterating).

You're trying to treat the object as an array ($objFoo['reeb'] = 'roob';). Now, for normal objects, that's not possible. If you want to do that, you need to implement the ArrayAccess interface. Note that you don't have to have an iterator defined in order to use that interface. All it does is provide the ability to access specific elements of an object using an array like syntax.

Note I said array like. You can't treat it like an array in general. sort functions will never work. However, there are a few other interfaces that you can implement to make an object more array like. One is the Countable interface which enables the ability to use the count() function on an object (count($obj)).

For an example in the core of combining all of these into one class, check out the ArrayObject class. Each interface in that list provides the ability to use a specific feature of the core. The IteratorAggregate provides the ability to use foreach. The ArrayAccess provides the ability to access the object with an array-like syntax. The Serializable interface provides the ability to serialize and deserialize the data in a specific manor. The Countable interface allows for counting the object just like an array.

So those interfaces don't affect the core functionality of the object in any way. You can still do anything with it that you could do to a normal object (such as $obj->property or $obj->method()). What they do however is provide the ability to use the object like an array in certain places in the core. Each provides the functionality in a strict scope (Meaning that you can use any combination of them that you would like. You don't need to be able to access the object like an array to be able to count() it). That's the true power here.

Oh, and assigning the return value of new by reference is deprecated, so there's no need to do that...

So, as for your specific problem, here's one way around it. I simply implemented the ArrayAccess interface into your class so that $objFoo['reeb'] = 'roob'; will work.

class foo implements ArrayAccess, IteratorAggregate {

    public $_array = array('foo'=>'bar', 'baz'=>'quux');

    public function getIterator() {
        return new ArrayIterator($this->_array);
    }

    public function offsetExists($offset) {
        return isset($this->_array[$offset]);
    }

    public function offsetGet($offset) {
        return isset($this->_array[$offset]) ? $this->_array[$offset] : null;
    }

    public function offsetSet($offset, $value) {
        $this->_array[$offset] = $value;
    }

    public function offsetUnset($offset) {
        if (isset($this->_array[$offset]) {
            unset($this->_array[$offset]);
        }
    }
}

Then, you can try your existing code:

$objFoo = & new foo();

foreach ( $objFoo as $key => $value ) {
    echo "key: $key, value: $value\n";
}

$objFoo['reeb'] = "roob";

foreach ( $objFoo as $key => $value ) {
    echo "key: $key, value: $value\n";
}

And it should work fine:

key: foo, value: bar
key: baz, value: quux
key: foo, value: bar
key: baz, value: quux
key: reeb, value: roob

You could also "fix" it by changing the $objFoo->_array property directly (just like regular OOP). Simply replace the line $objFoo['reeb'] = 'roob'; with $objFoo->_array['reeb'] = 'roob';. That'll accomplish the same thing....

  • Note that this is the default behavior. You could hack together an inner iterator (the iterator returned by IteratorAggreagate::getIterator) that does depend upon the original objects state. But that's not how it's typically done
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top