Can't write new elements to IteratorAggregate? - php

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

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

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

Related

What Interface to implement so an object behave like an array [duplicate]

I have a class called Collection which stores objects of same type.
Collection implements array interfaces: Iterator, ArrayAccess, SeekableIterator, and Countable.
I'd like to pass a Collection object as the array argument to the array_map function. But this fails with the error
PHP Warning: array_map(): Argument #2 should be an array
Can I achieve this by implementing other/more interfaces, so that Collection objects are seen as arrays?
The array_map() function doesn't support a Traversable as its array argument, so you would have to perform a conversion step:
array_map($fn, iterator_to_array($myCollection));
Besides iterating over the collection twice, it also yield an array that will not be used afterwards.
Another way is to write your own map function:
function map(callable $fn)
{
$result = array();
foreach ($this as $item) {
$result[] = $fn($item);
}
return $result;
}
Update
Judging by your use-case it seems that you're not even interested in the result of the map operation; therefore it makes more sense to use iterator_apply().
iterator_apply($myCollection, function($obj) {
$obj->method1();
$obj->method2();
return true;
});
array_map wants, as the name suggests, arrays. It's not called iterator_map after all. ;)
Apart from iterator_to_array(), which produces a potentially large temporary array, there's no trick to make iterable objects work with array_map.
The Functional PHP library has a map implementation which works on any iterable collection.
If you're not interested in creating a new array that is a function mapped over the original array, you could just use a foreach loop (because you implement Iterator).
foreach($item in $myCollection) {
$item->method1();
$item->method2();
}
if you actually want to use map, then I think you'll have to implement your own. I would suggest making it a method on Collection, eg:
$mutatedCollection = $myCollection->map(function($item) {
/* do some stuff to $item */
return $item;
});
I would ask yourself if you really want to use map or do you really just mean foreach
I came up with the following solution:
//lets say you have this iterator
$iterator = new ArrayIterator(array(1, 2, 3));
//and want to append the callback output to the following variable
$out = [];
//use iterator to apply the callback to every element of the iterator
iterator_apply(
$iterator,
function($iterator, &$out) {
$current = $iterator->current();
$out[] = $current*2;
return true;
},
array($iterator, &$out) //arguments for the callback
);
print_r($out);
This way, you can generate an array without iterating twice as you would to with the approach like:
$iterator = new ArrayIterator(array(1,2,3));
$array = iterator_to_array($iterator); //first iteration
$output = array_map(function() {}, $array); //second iteration
Good luck!
I just stumbled upon this question and I managed to cast the collection to an array to make it work:
array_map($cb, (array) $collection);
disclaimer For the original question this might not be a suitable option but I found the question while looking to solve a problem which I solved with this solution. I would recommend using a custom iterator map where possible/viable.
another option is to do something like this:
foreach($collection as &$item) {
$item = $cb($item);
}
which will mutate the underlying collection.
EDIT:
It has been pointed out that casting to an array can have unwanted side effects. It would be better to add a method to your collection to return the array from the iterator, and traverse that, or otherwise add a map method which accepts a callback and run a loop on the underlying iterator.

Is there equivalent for __toString() method for other simple types in PHP?

_toString() is called when an object is used as string. How can I do something similar for numerical values, something like __toInt(), or __toArray(). Do such methods exist? Is there a work around? Is it a bad idea to use something like that even if there is a workaround for it?
There is no __toArray magic-method (just check the ones that exist here), but then, there shouldn't be, IMO.
Though people have asked for a magic toArray method, it doesn't look like such a method will be implemented any time soon.
Considering what objects are for, and how we use them, a toInt method wouldn't make much sense, and since all objects can be cast to an array, and can be iterated over, I see very little point in using __toArray anyway.
To "convert" on object to an array, you can use either one of the following methods:
$obj = new stdClass;
$obj->foo = 'bar';
var_dump((array) $obj);
//or
var_dump(json_decode(json_encode($obj), true));
This can be done with both custom objects, as stdClass instances alike.
As far as accessing them as an array, I can't see the point. Why write a slow magic method to be able to do something like:
$bar = 'foo';
$obj[$bar];
if you can do:
$obj->{$bar}
or if you can do:
foreach($obj as $property => $value){}
Or, if you need something a tad more specific, just implement any of the Traversable interfaces.
And for those rare cases, where you want an object to produce an array from specific properties in a very particular way, just write a method for that and call that method explicitly.
class ComplexObject
{
private $secret = null;
private $childObject = null;
public $foo = null;
//some methods, then:
public function toArray()
{//custom array representation of object
$data = array();
foreach($this->childObject as $property => $val)
{
if (!is_object($this->childObject->{$property}))
{
$data[$property] = $val;
}
}
$data['foo'] = $this->foo;
return $data;
}
//and even:
public function toJson()
{
return json_encode($this->toArray());
}
}
Ok, you have to call these methods yourself, explicitly, but that's not that hard, really... is it?

ArrayObject doesn't allow me to unset a value, while iterating over it

I've got this notice:
ArrayIterator::next(): Array was modified outside object and internal position is no longer valid in /var/www...
which is produced by this code, at the begining of the foreach loop. Together with the notice, the foreach loop starts iterating all over again. In other words, the internal position is reset whenever this thing happens. But acording to php manual, ArrayObject is using ArrayIterator by default.
And manual says this about ArrayIterator
This iterator allows to unset and modify values and keys while iterating over Arrays and Objects.
Am I missing something here? I found some bugreports about ArratIterator, but not this kind. Is it a bug or is it my bad?
version: PHP Version 5.3.10-1ubuntu3.4
<?php
//file 1:
// no namespace
abstract class holder extends \ArrayObject{
// abstract function init();
public function __construct($init){
parent::__construct($init, 1);
}
}?>
<?php
//file 2:
namespace troops;
class holder extends \holder{
public function __construct(){
parent::__construct($this->init());
}
private function init(){
return array( /*... some data from db ...*/ );
}
public function saveData(){
foreach($this as $k => $v){
$this->save($v);
if($v->number_of_items==0) {
unset($k);
// $this->offsetUnset($k); // tryed both
}
}
}
}
?>
ArrayObject implements IteratorAggregate which means it has a method called getIterator() which returns an iterator.
php's foreach loop will automatically retrieve an iterator by calling the getIterator() method for you to retrieve an iterator to iterate over. This is convenient, but you need to get a reference to this iterator in order to call the offsetUnset() method on the iterator itself. The key thing here is you must call the iterators offsetUnset() method, not the ArrayObjects offsetUnset() method.
$ao = new ArrayObject();
$ao[] = 9;
$iter = $ao->getIterator();
foreach ($iter as $k => $v)
$iter->offsetUnset($k); // no error
The underlying ArrayObject that the iterator is iterating over will be mutated, so if you have more than one active iterator at the same time over the same Arrayobject, you'll still encounter the same error.
The rationale for this is likely so that the iterators can be memory efficient and not have to copy the underlying ArrayObject, because a copy is the only simple solution to dealing with the complexity of deciding what the current iterator position should be when things are added to or deleted from the underlying array.
In case you remove the last record from array than the next foreach loop can fail (internaly calling $this->next() )
Forexample when i have array of one item and I will unset it, than next foreach fails.
So it helped me to test the validy and break in this case the next loop.
deleteOffset = true;
foreach ($iterator as $key => $value)
{
if ($deleteOffset && $iterator->offsetExists($key) )
{
$iterator->offsetUnset($key);
}
//if remove last record than the foreach ( $this->next() ) fails so
//we have to break in this case the next ->next call
//after any ->offsetUnset calls
if (!$iterator->valid()) break;
}
This took me so long! I used the recipe on this page to (unsuccessfully) clear-out the contents of an ArrayObject's storage, then I found this little diddy..
$this->exchangeArray(array());
magic!

Array of Objects in PHP

I've created a class that keeps some information in its attributes. It contains add() method that adds a new set of information to all of the present in this class attributes.
I'd like its objects to behave like array offsets. For example, calling:
$obj = new Class[0];
would create the object containing the first set of information.
I'd also like to use foreach() loop on that class.
The changes of attributes should be denied from outside of the class, but I should have access to them.
Is that possible?
What you need is ArrayObject it implements IteratorAggregate , Traversable , ArrayAccess , Serializable , Countable altogether
Example
echo "<pre>";
$obj = new Foo(["A","B","C"]);
foreach ( $obj as $data ) {
echo $data, PHP_EOL;
}
echo reset($obj) . end($obj), PHP_EOL; // Use array functions on object
echo count($obj), PHP_EOL; // get total element
echo $obj[1] ; // you can get element
$obj[0] = "D"; // Notice: Sorry array can not be modified
Output
A
B
C
AC
3
B
Class Used
class Foo extends ArrayObject {
public function offsetSet($offset, $value) {
trigger_error("Sorry array can not be modified");
}
}
This is how you can create multiple instance with different constructor values.
$objConfig = array(
array('id'=>1 , 'name'=>'waqar') ,
array('id'=>2 , 'name'=>'alex')
);
$objects = array();
for($i=0; $i<count($objConfig) ; $i++)
{
$objects[$i] = new ClassName($objConfig[$i]);
}
You need to implement ArrayAccess interface, examples are pretty straightforward.
Anyway I really discourage you from mixing classes and array behaviour for bad design purposes: array-wise accessing should be used just to keep syntax more concise.
Take full advantage of classes, magic methods, reflection: there's a bright and happy world out there, beyond associative arrays.
In this case, why do you not just have an array of your class instances? A very simple example:
/**
* #var MyClass[]
*/
$myClasses = array();
$myClasses[] = new myClass();
Or alternatively use one of the more specialised SPL classes, here: http://php.net/manual/en/book.spl.php, such as SplObjectStorage (I haven't had a need for this, but it looks like it might be what you need)
Finally, you could roll your own, by simply creating a class that extends ArrayAccess and enforces you class type?
It really depends on what you need, for the vast majority of cases I would rely on storing classes in an array and enforcing any business logic in my model (so that array values are always the same class). This may be less performant, but assuming you're making a web app it is highly unlikely to be an issue.

php objects define and set attributes in constructor

Is it possible to define attributes to a class in the constructor.
For instance I pass a Associative Array to a class constructor and I want the attributes to that class to be declared and set based on what is in the Associative Array.
TIA
class Foo{
function __construct($arr){
foreach($arr as $k => $v)
$this->$k = $v;
}
}
You can review the constructor/desctructor manual and properties manual.
to be noted since you don't define the properties in the class all are set to public which IMHO is kind of dangerous. I think it might be possible to achieve the same thing using the reflection. I just checked more in depth the reflection and it is not possible (with PHP5), since it would make sense to be able to do that from the reflection it might come with PHP6.
full sample
<?php
class Foo{
function __construct($arr){
foreach($arr as $k => $v)
$this->$k = $v;
}
function getBar(){
return $this->bar;
}
}
$bar = new Foo(array(
'bar' => 'bar',
'foo' => 'foo'
)
);
var_dump($bar->bar);
?>
Yes, you can do that. off top of my head
public function __constructor($data){
foreach($data as $k=>$v){
$this->{$k} = $v;
}
}
This should do it, and of course you need to math define the fields.
I m not too sure if it s {$k} or ${k} or ${$k} but either of these should work.
You can certainly do this, as RageZ describes above, but I don't think I would recommend doing it. What this does is creates too loose of a "contract" between the users of this class - i.e. nobody really knows which properties the class has.
Instead of defining the properties on the fly, I would bet that you have a pre-defined set of "object property sets" that could in turn define a class hierarchy.
So instead of doing
if I get an array with "a = 2 and b =
3", create object with $obj->a = 3
and $obj-b = 3
if I get an array with
"c = 5", create an object with
$obj->c = 5
Do this:
define a class for MyObjectOfTypeOne, that has properties $a and $b
define a class for MyObjectOfTypeTwo that has property $c
Then, use the Factory pattern to generate the right type of object depending on the parameters passed to the static factory method.

Categories