Weird PHP behaviour - What's going on? [duplicate] - php

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Strange behavior Of foreach
Just came across this bug recently in a PHP app. Not sure what's going on.
Basically, it shows up when using a combination of two foreach (one with &, one without).
Here's a test code that reproduce the problem:
$items = array(
array('id'=>1, 'name'=>'foo', 'value'=>150),
array('id'=>2, 'name'=>'bar', 'value'=>190)
);
foreach($items as &$item)
{
$item['percentage'] = $item['value'] * 0.75;
}
var_dump($items); // All Good
foreach($items as $item)
{
var_dump($item); // Shows 1st item twice
}
The second foreach loop runs the block twice, as expected, but $item remains stuck on the first item.
I understand this is likely caused by the use of the reference & in the first loop but I don't see why it should behave like this..
Any idea? is that a bug?
Getting the same result on 5.3.8, 5.3.10 & 5.4

Firstly, it is not a bug as Rasmus said. See https://bugs.php.net/bug.php?id=29992
In this, right implementation of modifying array with its loop variable with &.
<?php
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
$value = $value * 2;
}
// $arr is now array(2, 4, 6, 8)
unset($value); // break the reference with the last element
var_dump($arr); // All Good
foreach($arr as $value) {
var_dump($value); // All good
}
?>

This is odd PHP behavior that's been around pretty much forever, and it happens when you mix the use of a variable as reference then not reference like you did.
I deal with it with a naming convention as follows: When I am using foreach with a &$item, I name it like &$refItem. This keeps me from mixing types.

You need to unset the pointer after using foreach with a referenced array.
http://php.net/unset

This may be something that look more understand the problem of foreach
$last_value_of_first_foreach = 1;
$item = 2;
$c = 3;
$item = &$last_value_of_first_foreach ; // Think that this statement is first foreach loop
// Here $item is pointer to $last_value_of_first_foreach
// To Better understanding, let change the name ($reference_to_last_value = $item;)
now, the new loop is
$item = $c;
// Here, what it do is update value where the $item pointer refer to
// (mean $last_value_of_first_foreach )
// so, at here $last_value_of_first_foreach has value of $c
Now, back to your case, from the first foreach, the $item reference to the last element of array. now, when you assign something to $item in second foreach, what it do, is put something inside that one.
In the end of first loop
$item is pointer to $items[1]
The first of second loop
it will push the first element to the location where the $item point to (that mean $items[1], so that why $items[1] is replaced by $items[0].
In case you want to prevent this one, just unset the $item variable before next time usage.

This is a normal, not a weird behavior. Just read about reference here.
When you add & in front of a variable, you store the reference to a variable. So, when you re-use it, it will also change the contents of the referenced variable.
foreach($items as &$item) // You have $item here, prefixed with &.
{
$item['percentage'] = $item['value'] * 0.75;
}
var_dump($items);
foreach($items as $item) // And your re-use it here.
{
var_dump($item);
}
To solve this, add unset($item) in the 1st loop:
foreach($items as &$item)
{
$item['percentage'] = $item['value'] * 0.75;
unset($item); // add this, because you use &$item previously.
}

Related

Strange foreach loop after modifying array

As I wrote some code, PHP confused me a little as I didn't expected the result of the following code:
$data = array(array('test' => 'one'), array('test' => 'two'));
foreach($data as &$entry) {
$entry['test'] .= '+';
}
foreach($data as $entry) {
echo $entry['test']."\n";
}
I think it should output
one+
two+
However the result is: http://ideone.com/e5tCsi
one+
one+
Can anyone explain to me why?
This is expected behaviour, see also https://bugs.php.net/bug.php?id=29992.
The reference is maintained when using the second foreach, so when using the second foreach the value of $entry, which points still to $data[1], is overwritten with the first value.
P.s. (thanks to #billyonecan for saying it): you need to unset($entry) first, so that your reference is destroyed.
This is mentioned specifically in the documentation for foreach. You should unset the loop variable when it gets elements of the array by reference.
Warning
Reference of a $value and the last array element remain even after the
foreach loop. It is recommended to destroy it by unset().

Unsetting elements from array within foreach

I'm currently working on a generic form creation class and had an issue yesterday.
I made a snippet to reproduce the problem.
Essentially I want to delete elements that are grouped from the original elements array after the whole group has been drawn and I'm doing this while looping over the elements array.
The code snippet should cover the problem, am I missing something here? From my knowledge deleting an element while foreach is completely safe and legal since foreach internally only uses a copy that may be modified during the loop.
$ids = array('a' => array(), 'b' => array(), 'c' => array());
$groups['g1'] = array('a', 'c');
foreach($ids as $id => $element) {
//var_dump($ids);
$g_id = '';
// search the id in all groups
foreach($groups as $group_id => $group) {
if(in_array($id, $group)) {
$g_id = $group_id;
break;
}
}
// element is part of a group
if($g_id !== '') {
//echo $g_id;
// element a and c gets unset within loop and should not be in $ids anymore
foreach($groups[$g_id] as $field_id) {
unset($ids[$field_id]);
echo $field_id;
}
unset($groups[$g_id]);
} else {
if($id === 'a' || $id === 'c')
echo $id;
}
}
Element 'c' gets unset within the foreach(groups ..) loop but is afterwards again outputted in the else branch. Also when i var_dump($fields) at the beginning i always get 'a', 'b' and 'c' inside. I'm using PHP 5.4.7.
Thanks in advance
EDIT: i made a mistake in the sample code, its now updated. All comments about using the wrong index (it would have been 0,1 etc) were correct of course.
The values when using var_dump are unset now, but i still get into the else with 'c' one time.
EDIT2:
Im not done with the original code but after reading through the comments I currently came up with following solution to the posted code snippet above:
$ids=array("a"=>array(),"b"=>array(),"c"=>array(),"d"=>array(),"e"=>array());
$groups=array(array("a"),array("c", "e"));
array_walk($groups,function($v,$i)use(&$ids){
$in_both = array_intersect(array_keys($ids),$v);
//var_dump($in_both);
foreach($in_both as $b) {
unset($ids[$b]);
}
});
print_r($ids);
or
$ids=array("a"=>array(),"b"=>array(),"c"=>array(),"d"=>array(),"e"=>array());
$groups=array(array("a"),array("c"));
array_walk($ids,function($v,$i)use(&$ids, $groups){
$in_both = array();
foreach($groups as $g) {
if(in_array($i,$g)) {
$in_both = array_intersect(array_keys($ids),$g);
}
}
foreach($in_both as $b) {
unset($ids[$b]);
}
});
print_r($ids);
Using a foreach does not work for me in this case, because i need to change the $ids array while the loop is iterating over it.
In the very most basic situation a code something like this:
$ids = array('a', 'b');
while(count($ids)) {
array_pop($ids);
echo 'pop';
}
echo 'empty';
Allthough foreach can change the original values from the array it will not change the copy of the array used for the iteration as nl-x already stated.
Thanks to Passerby for the idea of using array_walk for this.
EDIT3:
Updated code snipped once more. The second snipped allthough behaves undefined as well. Deleting elements from an array while iterating over its seems to be a bad idea.
Chris, if I understand correctly, you don't expect 'C' to be outputted in the else branch?
But it should be outputted. Your logic is:
you do foreach ids and start with id 'a'.
then you clear ids a and c from ids and delete the group g1 that contained 'a'. During this step the deleted ids will be outputted, being a and c. (Clearing a and c from ids will have no impact on the foreach($ids as $id) as foreach will continue with the untouched copy even after ids array has been cleared.)
then you do id 'b': it is not found in any group. (actually, there isn't any group left by now anyway)
so for 'b' you enter the else branch. But the if() inside the else branch prevents output
then you do id 'c', which is also not found in any group, because you have already deleted group g1! There are no groups left, remember?
so for 'c' you also enter the else branch. And this time the if() inside the else branch allows the output! The output being just c
So the total output is indeed acc.
It is good to know that a foreach() that continues with a untouched copy even after its elements were cleared, is a specific PHP thing. Other language do no necessarily do the same.
Spent some time reading your code, and I guess your procedure is:
For every element in $ids, check if it exists in some sub-array in $groups;
If it exists, delete everything in $ids that also exists in this sub-array.
Following the above logic, I come up with this:
$ids=array("a","b","c","d","e");
$groups=array(array("a","c"),array("c","e"));
array_walk($groups,function($v,$i)use(&$ids){
$ids=array_diff($ids,$v);
});
print_r($ids);//debug
Live demo
I'm double checking now. But I think unsetting an array with foreach is not really safe.
What I usually would do is take a foreach, and start with the highest indexes and descrease the index along the way. for($i = count($arr)-1; $i >= 0; $i--) { unset($array[$i]); }
I'll edit this post in a few minutes.
edit: i was confused. The for with $i++ is indeed the culprit. foreach is safe (in php! not in all languages)
<?php
$arr = Array(1,2,3,4,5,6,7,8,9,10);
foreach ($arr as $key=>$val)
unset($arr[$key]);
echo implode(',',$arr); // returns nothing
$arr = Array(1,2,3,4,5,6,7,8,9,10);
for ($i=0; $i<count($arr); $i++)
unset($arr[$i]);
echo implode(',',$arr); // returns 6,7,8,9,10
$arr = Array(1,2,3,4,5,6,7,8,9,10);
for ($i=count($arr)-1; $i>=0; $i--)
unset($arr[$i]);
echo implode(',',$arr); // returns nothing
?>
$ids[$field_id] does not exist, you are using the value instead of the key.
You should simply unset using the right key :
if (in_array($field_id, $ids))
unset($ids[array_search($field_id, $ids)]);
If you want to remove the element from the array, shouldn't you 'splice' it out instead with array_splice?
From the PHP manual: http://php.net/manual/en/function.array-splice.php
Your foreach wont make any changes since a copy of array is used.. you will need to use pass by reference in order for this to work. one of the way is mentioned below
while(list($key,$value) = each($array)){
if(your reason to unset)
unset($array[$key]);
}
this will remove the element from the array.

push into array while it is being looped

Here's a snippet of my code. I understand that it is generally advised against to modify an array whilst it is being looped, however in this scenario I think it is the right thing to do and saves resources.
$levels = array($post);
foreach ($levels as $level) {
$next_sibling = get_next_sibling_page($level);
if ($next_sibling) {
$next_page = $next_sibling;
break 1;
} else if ($level->post_parent) {
array_push($levels, get_page($level->post_parent));
}
}
The idea is that the foreach would reiterate with the pushed value if ($level->post_parent), I can see that the if statement is resolving to true and the array is being pushed to however the foreach does not reiterate and only runs the one time.
Does anyone know how I can recursively continue my foreach until $level->post_parent resolves to false?
To modify array which is being used in foreach pass array by referance.
foreach (&$levels as $level) {
.....
}

Using foreach effectively in PHP

So I don't think I'm making full use of the foreach loop. Here is how I understand foreach.
It goes like foreach(arrayyouwanttoloopthrough as onevalueofthatarray)
No counter or incrementing required, it automatically pulls an array, value by value each loop, and for that loop it calls the value whatever is after the "as".
Stops once it's done with the array.
Should basically replace "for", as long as dealing with an array.
So something I try to do a lot with foreach is modify the array values in the looping array. But in the end I keep finding I have to use a for loop for that type of thing.
So lets say that I have an array (thing1, thing2, thing3, thing4) and I wanted to change it....lets say to all "BLAH", with a number at the end, I'd do
$counter = 0;
foreach($array as $changeval){
$counter++;
$changeval = "BLAH".$counter;
}
I would think that would change it because $changeval should be whatever value it's at for the array, right? But it doesn't. The only way I could find to do that in a] foreach is to set a counter (like above), and use the array with the index of counter. But to do that I'd have to set the counter outside the loop, and it's not even always reliable. For that I'd think it would be better to use a for loop instead of foreach.
So why would you use foreach over for? I think I'm missing something here because foreach has GOT to be able to change values...
Thanks
OH HEY One more thing. Are variables set in loops (like i, or key) accessible outside the loop? If I have 2 foreach loops like
foreach(thing as value)
would I have to make the second one
foreach(thing2 as value2) ]
or else it would have some problems?
You can use a reference variable instead:
foreach ($array as &$value)
{
$value = "foo";
}
Now the array is full of foo (note the & before $value).
Normally, the loop variable simply contains a copy of the corresponding array element, but the & (ampersand) tells PHP to make it a reference to the actual array element, rather than just a copy; hence you can modify it.
However, as #Tadeck says below, you should be careful in this case to destroy the reference after the loop has finished, since $value will still point to the final element in the array (so it's possible to accidentally modify it). Do this with unset:
unset($value);
The other option would be to use the $key => $value syntax:
foreach ($array as $key => $value)
{
$array[$key] = "foo";
}
To answer your second question: yes, they are subsequently accessible outside the loop, which is why it's good practice to use unset when using reference loop variables as in my first example. However, there's no need to use new variable names in subsequent loops, since the old ones will just be overwritten (with no unwanted consequences).
You want to pass by reference:
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value)
{
$value = $value * 2;
}
// $arr is now array(2, 4, 6, 8)
The extended foreach syntax is like this:
foreach ($array as $key => $value) {
}
Using the $key, you can index the original array:
foreach ($array as $key => $value) {
$array[$key] = "BLAH";
}
Incrementing from 0 to ...
foreach($array as $changeval){
if (!isset($counter)) { $counter = 0; }
$counter++;
$changeval = "BLAH".$counter;
}
Using the index/key of the ARRAY
foreach($array as $key => $changeval){
$changeval = "BLAH".$key;
}
You can use the key when looping with foreach:
foreach ($array as $key => $value)
{
$array[$key] = "foo";
}
But note that using a reference like Will suggested will be faster for such a case - but anyway, the $key => $value-syntax is quite useful sometimes.

How can I find out how many times a foreach construct loops in PHP, without using a "counter" variable?

If I have a foreach construct, like this one:
foreach ($items as $item) {
echo $item . "<br />";
}
I know I can keep track of how many times the construct loops by using a counter variable, like this:
$counter = 0;
$foreach ($items as $item) {
echo $item.' is item #'.$counter. "<br />";
$counter++;
}
But is it possible to do the above without using a "counter" variable?
That is, is it possible to know the iteration count within the foreach loop, without needing a "counter" variable?
Note: I'm totally okay with using counters in my loops, but I'm just curious to see if there is a provision for this built directly into PHP... It's like the awesome foreach construct that simplified certain operations which are clunkier when doing the same thing using a for construct.
No it's not possible unless your $items is an array having contiguous indexes (keys) starting with the 0 key.
If it have contiguous indexes do:
foreach ($items as $k => $v)
{
echo $k, ' = ', $v, '<br />', PHP_EOL;
}
But as others have stated, there is nothing wrong using a counter variable.
There's no easier way - that's kinda what count variables are for.
I'm assuming that you want to know the current count during the loop. If you just need to know it after, use count($items) as others have suggested.
You could tell how many time it WILL loop or SHOULD have looped by doing a
$loops = count($items);
However that will only work if your code does not skip an iteration in any way.
foreach loops N times, where N is just the size of the array. So you can use count($items) to know it.
EDIT
Of course, as noticed by Bulk, your loop should not break (or maybe continue, but I would count a continue as a loop, though shorter...)

Categories