With PHP 7.2, each
is deprecated. The documentation says:
Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.
I am working on adapting an e-commerce application and converting all while-each
loops to the (supposedly) equivalent foreach
.
As you can see below, I have already replaced all the reset
& while
loops with an equivalent foreach
.
It mostly worked fine. However, we had a customer with a very long list of items in her cart that tried to checkout and complained that she gets error 502 from the server.
I tried to reproduce that and found that only her cart fails, it takes over 2 minutes for the checkout page to load, and then times with a 502.
I then started debugging a lot of files I recently modified, trial and error, until I found out that the issue is with this specific file and specific function.
Whenever I switch the first foreach
loop back to the while
loop, the customer can load the checkout page in less than a second. Switching back to foreach
– and again it take minutes, but php times out before it ends execution.
I did perform tests of course on the output of that foreach
vs the while
loop (var_dump $products_id
and $this->contents
for example) and they all seem identical. I already rewrote the code to make it work smoothly and to stay PHP 7.2 compatible, but I still can’t figure out why this happens.
This is the full function:
function get_content_type() {
$this->content_type = false;
if ( (DOWNLOAD_ENABLED == 'true') && ($this->count_contents() > 0) ) {
// reset($this->contents);
// while (list($products_id, ) = each($this->contents)) {
foreach(array_keys($this->contents) as $products_id) {
if (isset($this->contents[$products_id]['attributes'])) {
// reset($this->contents[$products_id]['attributes']);
// while (list(, $value) = each($this->contents[$products_id]['attributes'])) {
foreach ($this->contents[$products_id]['attributes'] as $value) {
$virtual_check_query = tep_db_query("select count(*) as total from " . TABLE_PRODUCTS_ATTRIBUTES . " pa, " . TABLE_PRODUCTS_ATTRIBUTES_DOWNLOAD . " pad where pa.products_id = '" . (int)$products_id . "' and pa.options_values_id = '" . (int)$value . "' and pa.products_attributes_id = pad.products_attributes_id");
$virtual_check = tep_db_fetch_array($virtual_check_query);
if ($virtual_check['total'] > 0) {
switch ($this->content_type) {
case 'physical':
$this->content_type = 'mixed';
return $this->content_type;
break;
default:
$this->content_type = 'virtual';
break;
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed';
return $this->content_type;
break;
default:
$this->content_type = 'physical';
break;
}
}
}
} elseif ($this->show_weight() == 0) {
// reset($this->contents);
// while (list($products_id, ) = each($this->contents)) {
foreach (array_keys($this->contents) as $products_id) {
$virtual_check_query = tep_db_query("select products_weight from " . TABLE_PRODUCTS . " where products_id = '" . $products_id . "'");
$virtual_check = tep_db_fetch_array($virtual_check_query);
if ($virtual_check['products_weight'] == 0) {
switch ($this->content_type) {
case 'physical':
$this->content_type = 'mixed';
return $this->content_type;
break;
default:
$this->content_type = 'virtual';
break;
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed';
return $this->content_type;
break;
default:
$this->content_type = 'physical';
break;
}
}
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed';
return $this->content_type;
break;
default:
$this->content_type = 'physical';
break;
}
}
}
} else {
$this->content_type = 'physical';
}
return $this->content_type;
}
Thank you
EDIT: here is the array:
https://pastebin.com/VawX3XpW
The issue has been tested and reproduced on all the configurations I have tried:
1) High end windows 10 pc + WAMP (Apache 2.4 + MariaDB 10.2 + PHP 5.6+/7+/7.1+/7.2+)
2) High end CentOS/cPanel server + Litespeed + MariaDB 10.1 + PHP 5.6+
Just to emphasize, I am not looking to rewrite the code or emulate each
and then rewrite the code, as we won’t learn much from that. I am simply trying to find a logical explanation or a way to solve/debug this mystery. Maybe someone, somewhere, ever bumped into such an issue and can shed some light on this.
UPDATE 01/Aug/2018
I have been trying to debug this for days, eventually noticed something interesting. I added “echo points” and exit
on the first foreach
loop and while
loop like so:
function get_content_type() {
$this->content_type = false;
if ( (DOWNLOAD_ENABLED == 'true') && ($this->count_contents() > 0) ) {
// reset($this->contents);
// while (list($products_id, ) = each($this->contents)) { echo '1 ';
foreach(array_keys($this->contents) as $products_id) { echo '1 ';
if (isset($this->contents[$products_id]['attributes'])) { echo '2 ';
// reset($this->contents[$products_id]['attributes']);
// while (list(, $value) = each($this->contents[$products_id]['attributes'])) {
foreach ($this->contents[$products_id]['attributes'] as $value) { echo '3 ';
$virtual_check_query = tep_db_query("select count(*) as total from " . TABLE_PRODUCTS_ATTRIBUTES . " pa, " . TABLE_PRODUCTS_ATTRIBUTES_DOWNLOAD . " pad where pa.products_id = '" . (int)$products_id . "' and pa.options_values_id = '" . (int)$value . "' and pa.products_attributes_id = pad.products_attributes_id");
$virtual_check = tep_db_fetch_array($virtual_check_query);
if ($virtual_check['total'] > 0) {
switch ($this->content_type) {
case 'physical':
$this->content_type = 'mixed'; echo '4 ';
return $this->content_type;
break;
default:
$this->content_type = 'virtual'; echo '5 ';
break;
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed'; echo '6 ';
return $this->content_type;
break;
default:
$this->content_type = 'physical'; echo '7 ';
break;
}
}
}
} elseif ($this->show_weight() == 0) {
// reset($this->contents);
// while (list($products_id, ) = each($this->contents)) {
foreach (array_keys($this->contents) as $products_id) {
$virtual_check_query = tep_db_query("select products_weight from " . TABLE_PRODUCTS . " where products_id = '" . $products_id . "'");
$virtual_check = tep_db_fetch_array($virtual_check_query);
if ($virtual_check['products_weight'] == 0) {
switch ($this->content_type) {
case 'physical':
$this->content_type = 'mixed'; echo '8 ';
return $this->content_type;
break;
default:
$this->content_type = 'virtual'; echo '9 ';
break;
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed'; echo '10 ';
return $this->content_type;
break;
default:
$this->content_type = 'physical'; echo '11 ';
break;
}
}
}
} else {
switch ($this->content_type) {
case 'virtual':
$this->content_type = 'mixed'; echo '12 ';
return $this->content_type;
break;
default:
$this->content_type = 'physical'; echo '13 ';
break;
}
}
} exit; //Exiting from the loop to check output
} else {
$this->content_type = 'physical';
}
return $this->content_type;
}
When I was running the loop with while
, the output I got was just “1 13” once, meaning the loop is running only once and stops.
However, when I changed it to foreach
, I got a long list of “1 13 1 13 1 13…”, meaning it is looping many times. I have gone to investigate further if there is any difference between breaks
of while
loops and foreach
loops, and I still couldn’t find any supporting information. I then rewrote the last break;
to be break 2;
and tested the foreach
again and this time it seems to have been running just once, just like when it was a while
loop with a break;
(not break 2;
)
EDIT: Just to clarify – there is no difference between while break
s and foreach break
s. They work the same way.
UPDATE #2:
I have revised } elseif ($this->show_weight() == 0) {
to } elseif (2 == 0) {
and the while
loop now runs as many times as the foreach
loop.
var_dump($this->show_weight());
results float 4466.54
.
The issue still doesn’t make any sense to me.
Thanks again
4
Answers
As a backup solution, What about a drop in replacement performing the same action?
This is actually a very simple algorithmic problem, and it has to do with the fact that when
show_weight()
is0
you are looping the same array (edit: and based on your comments,show_weight()
itself is also looping the same array).TL;DR With
while
all those loops were sharing the same internal pointer and influencing each others. Withforeach
each loop is independent and therefore it runs way more iterations, hence the performance issue.Since an example is worth a thousand words, hopefully the following code will make things clearer:
This will output:
As you can see, for an array of size 3,
foreach
takes 3² iterations, whilewhile
takes only 3. That’s your performance problem.Why was
while
faster?Because at the end of the second (inner)
while
, the internal pointer of$array
would have been pointing to the end of the array, therefore the first (outer)while
would simply stop.With a
foreach
, since your are using the result of 2 different calls toarray_keys
, you are using 2 different arrays that do no share the same internal pointer, therefore there is no reason for the loop to stop. A simplereturn
after the second (inner)foreach
should solve the problem.I don’t quite understands the main problem here but, you can start by optimizing your foreach loops using key value approach:
using return in switch will break the whole function, exit also.
if you want to break a loop from a switch statement use:
break 2; will break switch and the parent foreach
break 3; will break switch and the first and the second parent foreach
In your original code, if the following returns true in the first iteration:
The code loops through $this->contents using each(), setting the array pointer on $this->contents to the end. So when we get back around to the first while() statement, it assumes it’s already done, because the next call of each($this->contents) returns false.