Showing posts with label PHP5. Show all posts
Showing posts with label PHP5. Show all posts

Wednesday, February 17, 2010

PHP: References To Array Elements Are Risky

References to array elements can bite! And it is not only the case with referencing in foreach loop. It seems that creating a reference to an array element replaces that element itself with a reference. If you then copy such an array and change the elements inside copy you can overwrite original value!

All of the presented code was tested on Mac (PHP 5.2.11), Linux (PHP 5.2.6-1+lenny4) and Windows XP (PHP 5.3.0) using PHP cross platform testing lab on Mac based on VirtualBox.

Changing copy affects original array

Sounds impossible? I agree. I couldn't believe it myself. Nevertheless here is the proof:
Example 1
$a = array('one', 'two', 'three', 'four'); 
$a2 = &$a[2]; 
 
$b = $a; 
$b[1] = 'two again'; 
$b[2] = 'reference bites'; 
 
var_dump($a, $b);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  &string(15) "reference bites"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(9) "two again"
  [2]=>
  &string(15) "reference bites"
  [3]=>
  string(4) "four"
}

Changing copy of a copy affects original array too

If you look at the dump carefully you will see that $a[2] and $b[2] are displayed as references here. That would mean the element has been replaced with a reference. And if you copy a reference you just get what? Same reference, right? So any copy of $a would contain that reference. Going forward any copy of a copy of $a would contain that same reference. Let's check it:
Example 2
$a = array('one', 'two', 'three', 'four'); 
$a2 = &$a[2]; 
 
$b = $a; 
$c = $b; 
$c[2] = 'references bites more'; 
$b[1] = 'two again'; 
$a[0] = 'one more'; 
var_dump($a, $b, $c);
The above example will output:
array(4) {
  [0]=>
  string(8) "one more"
  [1]=>
  string(3) "two"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(9) "two again"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  &string(21) "references bites more"
  [3]=>
  string(4) "four"
}

Tricky "foreach" with reference explained

As you can see, the only element affected is the one referenced. That can lead to serious potential problems. Working on a copy of an array with scalar elements seems to be not as safe as one may thought. However, that explains one of the biggest pitfalls of iterating arrays in PHP: foreach with reference. The following is rather common knowledge:
Example 3
$a = array('one', 'two', 'three', 'four'); 
foreach ($a as &$v) {} 
foreach ($a as $v) {} 
var_dump($a);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
  [3]=>
  &string(5) "three"
}
But what was the explanation for that, again? It is quite clear that $v keeps reference to $a[3] after the foreach loop is finished. So what values does $v (effectively $a[3]) get within the next foreach loop? Let's see:
Example 4
$a = array('one', 'two', 'three', 'four'); 
foreach ($a as &$v) {} 
foreach ($a as $v) { 
 var_dump($a[3]); 
}
The above example will output:
string(3) "one"
string(3) "two"
string(5) "three"
string(5) "three"
Well, it's quite obvious. Or is it? As the foreach manual page states: "unless the array is referenced, foreach operates on a copy of the specified array and not the array itself". I would assume that "array is referenced" means array elements are referenced like this: foreach ($a as &$v) {} Apparently that is not the case with the second loop and it should work on a copy. Let's have a look what exactly would happen if the second foreach loop really worked on a solid copy:
Example 5
$a = array('one', 'two', 'three', 'four'); 
$b = $a; 
foreach ($a as &$v) {} 
foreach ($b as $v) {} 
var_dump($a);
The above example will output:
array(4) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
  [3]=>
  &string(4) "four"
}
I imagine you did expect this result. Beware references to array elements and thanks for reading. I hope you found this article useful. Please don't think twice before you leave your comment.

Friday, January 22, 2010

PHP: Array Pointer in Undefined State

Array pointer in PHP may become "undefined" and that is when you try to move it after the last of before the first element. But what is more surprising - copying an array remaining in that state... resets the pointer.

First let's create a messy array and see how it dumps:

$a = array (5 => 'key 5', 'key after 5', 3 => 'key 3', 'a' => 'key a', 1 => 'key 1', 0 => 'key 0', 'key after 0',);
var_dump($a);

The result (as expected):

array(7) {
  [5]=>
  string(5) "key 5"
  [6]=>
  string(11) "key after 5"
  [3]=>
  string(5) "key 3"
  ["a"]=>
  string(5) "key a"
  [1]=>
  string(5) "key 1"
  [0]=>
  string(5) "key 0"
  [7]=>
  string(11) "key after 0"
}

When Array Pointer Becomes Undefined

Let's try and go beyond the last element in the array:

// move the pointer to the last element
end($a);

// move the pointer one element further
next($a);

// ... and see where we are
var_dump(key($a), current($a));

// try to go back
prev($a);

// ... and see where we are
var_dump(key($a), current($a));

The above code will produce the following result:

NULL
bool(false)

NULL
bool(false)

Which simply means that the array pointer becomes "undefined" if moved after last element. In fact the pointer is not "after" the last element because you cannot move it back with

prev()
function. It simply remains in undefined state.

Copying Array Resets The Pointer

When you copy an array, the array pointer is copied as well and it points to the same element as in the original. But what happens when the pointer is undefined? Will the copy inherit that as well? Let's consider the code:

// copy the array
$b = $a;

// check state of the original
print ('original: key() and current()' . PHP_EOL);
var_dump(key($a), current($a));

// check state of the copy
print ('copy: key() and current()' . PHP_EOL);
var_dump(key($b), current($b));

And the result:

original: key() and current()
int(5)
string(5) "key 5"

copy: key() and current()
NULL
bool(false)

Just as anticipated, the copy has its pointer in undefined state. But what happened to the original? Apparently copying the array worked like

reset()
and moved the pointer to the first element! Needless to say, copying the array preserves the pointer's value if it's defined:

// move the pointer to the last element
end($a);

// move the pointer to one element before last
prev($a);

// see where we are
print ('key() and current()' . PHP_EOL);
var_dump(key($a), current($a));

// copy the array
$b = $a;

// check state of the original
print ('original: key() and current()' . PHP_EOL);
var_dump(key($a), current($a));

// check state of the copy
print ('copy: key() and current()' . PHP_EOL);
var_dump(key($b), current($b));

And the output:

key() and current()
int(0)
string(5) "key 0"

original: key() and current()
int(0)
string(5) "key 0"

copy: key() and current()
int(0)
string(5) "key 0"

Moving Array Pointer Before First Element

All the above applies to the situation when you move the pointer "before" the first element. But don't take my word for it - try it out.