views:

37

answers:

2

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
+1  A: 

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
ircmaxell
I'm confused. I didn't try to write during iteration; the `foreach` loop was closed before `$objFoo['reeb'] = "roob";`, yet it still gives an error. It should have been reset at that point, shouldn't it?
It was. The thing is that you are trying to write to the object of class `foo` like an array. That only works if you implement the `ArrayAccess` interface (which enables the `$obj[$key]` syntax). Otherwise it's not enabled. The error you gave is because you're trying to treat `$objFoo` like an array, but since it doesn't have the `ArrayAccess` interface, it doesn't know what to do, so it fatal errors. Add the `ArrayAccess` interface to `foo`, and implement the 4 required methods. Then it'll work fine... I'll edit my answer with an example...
ircmaxell
So then, in what contexts can you write to an `IteratorAggregate`?
Well, you can theoretically write to an IteratorAggregate if you build your own Iterator that takes in a reference and iterates over that (so that any changes to the inner iterator will be reflected in the Aggregate iterator). But like I said, that's not what Aggregate iterators are designed to do. If you need that (modifying the iterated instance while iterating), then why not just implement `Iterator` instead of `IteratorAggregate`...
ircmaxell
It sounds to me like it's more accurate to say that you *can't* write to an `IteratorAggregate` object as an array, so do to that, you would need to either implement `ArrayAccess` or write an iterator. I'm not trying to be pedantic; I just want to have a good understanding so I'm not walking around thinking there is some case where I *could* write to an `IteratorAggregate` when I actually can't. I would feel that I misunderstood `IteratorAggregate` if I thought I could write to it in some cases where it's just `IteratorAggregate` alone.
Well, I think you're confused here a bit. The `Iterator` and `IteratorAggregate` interfaces enable `foreach` functionality only. That's it (well, and manual iteration). So they don't "control" how the object behaves when not inside that loop... And when inside, it only affects the `$key => $value` part. That's where you can't write with an aggregate. You can't (well it won't work) with: `foreach ($obj as $key => }`. But they make no difference in how the object behaves outside of the "iteration" (foreach). Oh, and you can't write to any object as an array without AA...
ircmaxell
I accepted it, but I can't upvote it in good conscience. Saying "You cannot write to an IteratorAggregate while iterating... However it doesn't prevent writes in other contexts" makes it sound like `IteratorAccess` provides writing capability. It doesn't *prevent* writing -- but hey, you can write to any object, so long as that functionality is there, no? So someone else might come away thinking that `IteratorAggregate` provides write access when not iterating, in certain contexts. It seems to conflicts with "you can't write to any object as an array without AA"
I'm not saying the answer is wrong, rather I think it's unclear or confusing and someone could come away with a misunderstanding. I'm asking to confirm that `IteratorAggregate` does not *by itself* provide write capabilities, and without a clear no, I think someone could be confused.
@user151841: I've reworded my entire question. Does that do a better job?
ircmaxell
That's an incredible job. You, sir, have definitely earned an upvote :D
+2  A: 

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
salathe