Hey guys i've the following scenario
$col = new ArrayObject();
for ($i = 0; $i < 5; $i++)
{
$objItem = new stdClass;
$objItem->someVar = $i;
$col->append($objItem);
}
Now i want to remove some key via offsetUnset and did the following
foreach($col AS $key => $objItem)
{
if ($key == 2)
{
$col->offsetUnset($key);
}
echo $key.'\n';
}
The funpart now is the unexpected output
0
1
2
4
For a better illustration - if you put a continue in like
foreach($col AS $key => $objItem)
{
if ($key == 2)
{
$col->offsetUnset($key);
continue;
}
echo $key.'\n';
}
the output is
0
1
4
I never ever would've thought it bypasses the key number 3 in this ArrayObject just because i removed the previous item - it looks like the internal pointer was incremented twice or something like that...
Keep in mind i also tried something like
$it = $col->getIterator();
foreach($it AS $key => $objItem)
{
if ($key == 2)
{
$col->offsetUnset($key);
}
echo $key."\n";
}
the output is the same
Question:
Is this a normal behavior or do i overlook something crucial here?
pS: As nigel Ren pointed out this as a possible duplicate of Php, Spl, ArrayIterator, i've to point out i was looking here for an ArrayObject solution and didn't found any (i now know the ArrayIterator problem is probably the same) - but i would've never guessed that those are related to each other - in this respect i plead for not closing this question because it might help others too
The solution for this Bug is actually pretty simple and was posted here
by a user called Tsounabe.
I'm putting here his solution - only with one change - he was referring to an ArrayIterator and i was talking about an ArrayObject - but both are using the same function getArrayCopy
foreach($col->getArrayCopy() AS $key => $objItem)
{
if ($key == 2)
{
$col->offsetUnset($key);
continue;
}
echo $key.'\n';
}
Conclusion
The only satisfying solution here in order to avoid this bug is the use of getArrayCopy() which is documented on the official PHP Site under https://secure.php.net/manual/de/arrayobject.getarraycopy.php
In your code it is resetting object key after $col->offsetUnset($key); and continue with next key 3 after resetting it would the element whose ->someVar value is 4, that's why you get this output.
So you can simply use another iteration to print your object. it would work.
Live demo
Out put
0
1
3
4
Related
i'm sorry if my english not good.
I have a problem in my code. how I can stop access when in my array have duplicate. I mean, when one or more data in array have duplicate, user must repeat input from first page. and when array have not duplicate, user can access next page.
this is my code I have try.
$no_id=array('100100','100200','100300','100200','100200','100100');
$array = array();
foreach ($no_id as $key => $value)
{
if(isset($array[$value]))
{
$array[$value] += 1;
} else
{
$array[$value] = 1;
}
}
foreach ($array as $alert => $n)
{
if($n >= 2)
{
header('location:../../home');
} else
{
header('location:../../questionnair');
}
}
but when found the duplicate data (in array 100100 have two data and 100200 three data), the system still bring user to access questionnair page, not home.
thanks for helping.
I think the main problem is that you should always use exit() after using header() with location, otherwise the page will continue to execute.
This would still cause a problem though if the first value was a unique value as the first loop would always exit;.
You could fix this (only ever call header() if the value is > 2, then the default of all are unique is after the loop), but an alternative is to use array_count_values() which counts how many each value exists in the list and then use max() to find the most occurring one, then test against that...
$no_id=array('100100','100200','100300','100200','100200','100100');
$n = max(array_count_values($no_id));
if($n >= 2)
{
header('location:../../home');
exit;
} else
{
header('location:../../questionnair');
exit;
}
Update:
An alternative and slightly quicker version would be to use your original first loop, but as soon as it detects the value is already set, then it can stop working and just return then...
$no_id=array('100100','100200','100300','100200','100200','100100');
$array = array();
foreach ($no_id as $key => $value)
{
if(isset($array[$value]))
{
header('location:../../home');
exit;
} else
{
$array[$value] = 1;
}
}
header('location:../../questionnair');
exit;
If you just need to check if the array does have dupliate, you can use array_unique and compare the new array with the old array. If the arrays are not the same, it means there are duplicates.
With code:
$no_id = array('100100','100200','100300','100200','100200','100100');
$new_array = array_unique($no_id);
if (count($no_id) == count($new_array)) {
// 2 arrays have same number of items => they are equal => no duplicates
$redirect = "questionnair.php";
} else {
// 2 arrays have different number of items => they are not equal => duplicates
$redirect = "home.php";
}
header("location: {$redirect}");
NOTE
You have to redirect to an another PHP page (ex. home.php and not just home).
First of all, it's simplier to use array_unique function, which will remove all duplicated values.
After that you can make checks, if nothing was changed, then there is no duplicated items.
Like this:
$no_id=array('100100','100200','100300','100200','100200','100100');
$no_id_unique = array_unique($no_id);
if(count($no_id)===count($no_id_unique)){
header('location:../../questionnair');
}else{
header('location:../../home');
}
exit();
Next thing that you using ../ in path which means go to parent directory.
I don't know your environment, but must likely it's a bad idea.
According to documentation it's better to specify full url of page where user must be forwarded:
header("Location: http://www.example.com/home/");
Also, please be aware, that you need to prevent all others output from a script, especcially before the header() call. Read more here.
you can use foreach (array_unique($no_id) as $key => $value)
You can use array_count_values for this, not tested, but could be like:
$counts = array_count_values($no_id);
$duplicate = false;
foreach ($no_id as $value) {
if ($counts[$value] > 1) {
$duplicate = true;
break; // found a duplicate, no need to check further.
}
}
if ($duplicate === true) {
header('location:../../home');
} else {
header('location:../../questionnair');
}
Using array_flip()
$no_id = array('100100','100200','100300','100200','100200','100100');
$no_id_unique = array_flip($no_id);
if(count($no_id) === count($no_id_unique)){
header('location:../../questionnair');
}else{
header('location:../../home');
}
exit();
The typical algorithm of search until found, in PHP and for some arrays where we can't use the language array search functions (I think), could be implemented in this way:
$found = false;
while (list($key, $alreadyRP) = each($this->relatedProducts) && !$found) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
}
}
if (!$found) {
// Do something here
}
Please, take it as pseudocode, I didn't executed it. What I like about it, is that it gracefully stops if it is found what we are looking for.
Now, due to the "each" function is deprecated, I have to code something like this:
$found = false;
foreach ($this->relatedProducts as $alreadyRP) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
break;
}
}
if (!$found) {
// Do something here
}
To put a "break" statement inside a "for" loop is ugly in structured programming. Yes, it is optional, but if we avoid it, the "foreach" will go through all the array, which is not the most efficient way.
Any idea to recover the efficiency and structure that "each" gives in this case?
Thank you.
The beauty of the each() method is in the eye of the beholder, but there are other reasons to prefer foreach, including this interesting bit of information from the RFC that led to the deprecation of each()
The each() function is inferior to foreach in pretty much every imaginable way, including being more than 10 times slower.
If the purpose of the method is to // Do something here if the $rp is not found in $this->relatedProducts, I think a more "beautiful" way to handle it would be to extract the search through related products into a separate method.
protected function inRelatedProducts($id) {
foreach ($this->relatedProducts as $alreadyRP) {
if ($alreadyRP->getProduct()->getId() === $id) {
return true;
}
}
return false;
}
Moving the related products search into a separate method is advantageous because
It separates that functionality from the original method so that it becomes reusable instead of being tied to whatever // Do something here does.
It simplifies the original method so it can focus on its main task
$id = $rp->getProduct()->getId();
if (!$this->inRelatedProducts($id)) {
// Do something here
}
It simplifies the search code because if it's contained in its own method, you can just return true; as soon as you find a match, so you won't need to break, or to keep track of a $found variable at all.
On the other hand, if this was my project I would be looking for a way to remove the need for this method by populating $this->relatedProducts so that it's indexed by ID (assuming ID is unique there) so the determination could be reduced to
$id = $rp->getProduct()->getId();
if (isset($this->relatedProducts[$id])) { ...
If you're looking for a rewrite that doesn't involve extra variables, you can replace the each call with calls to current and next:
do {
$found = (current($this->relatedProducts)->getProduct()->getId() === $rp->getProduct()->getId());
} while (empty($found) && false !== next($array));
This is a mechanical translation, and it relies merely on the definition of each (emphasis mine):
Return the current key and value pair from an array and advance the array cursor
It also suffers the same deficiency of the original each version: it doesn't handle empty arrays.
That said, please don't use each, or any of its siblings, for new code. This from a guy who voted "No" on the RFC! Why? Because the performance sucks:
1017$ cat trial.php
<?php
$array = range(0, 999);
$begin = -microtime(true);
for ($i = 0; $i < 10000; $i++) {
reset($array);
$target = rand(333, 666);
do {
$found = (current($array) === $target);
} while (empty($found) && false !== next($array));
}
printf("%.5f\n", $begin + microtime(true));
$begin = -microtime(true);
for ($i = 0; $i < 10000; $i++) {
$target = rand(333, 666);
foreach ($array as $current) {
if ($current === $target) break;
}
}
printf("%.5f\n", $begin + microtime(true));
1018$ php -d error_reporting=-1 trial.php
8.87178
0.33585
That's nearly nine seconds for the next/current version while not even half a second for the foreach version. Just don't.
It looks like each is basically a version of current() and next()
http://php.net/manual/en/function.current.php
http://php.net/manual/en/function.next.php
each() gives the current array item, and moves to the next index.
current() gives the current array item, but doen't increment the index.
So, you can replace each() with current(), and inside your foreach use next() to shift the index up
next() gives the next item, and increments the index.
while (list($key, $alreadyRP) = current($this->relatedProducts) && !$found) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
}
next($this->relatedProducts);
}
This question already has answers here:
The 3 different equals
(5 answers)
Closed 6 years ago.
I am puzzled by an issue in my code and hoping someone else can help me shed some light on why my loop is omitting the first element (array[0]) of the array.
the Code
foreach ($a as $key => $val) {
for ($i=0; $i<count($val); $i++) {
$x = $i; //this helps me jump logical arguments without the use of else
// First Test
if (isset($val[$i+2]) && $x = $i) {
//Do a bunch of stuff
if (isset(the stuff done above)) {
// do other things and reset $i to jump through the array
$i=$i+2;
}
else {
unset($things);
unset($otherthings);
}
}
}
// Second Test
if (isset($val[$i+1]) && $x = $i) {
//Do a bunch of stuff
if (isset(the stuff done above)) {
// do other things and reset $i to jump through the array
$i=$i+1;
}
else {
unset($things);
unset($otherthings);
}
}
}
// Third and final test
if ($x = $i) {
//do other things
}
}
}
the Problem
I can't seem to understand why but the for loop or the IF statements (I am not 100% sure which one) fail to run through the first element of the array[0].
It runs fine from array[1] onward but even though i have tested that $x is indeed = to $i and therefore test 3 at the very least should work, the loop seems to run one loop past all the IF's and then start working from array[1].
What I have tried
I have changed for ($i=1; $i<$val; $i++) and this works fine (e.g. does not omit anything) from the start (but of course does not solve my problem as I am still missing array[0])
I have tested in the code if echo $val[0] prints out at the beginning of the code and it does
I have also tested $x = $i and these also work
It is one of those issues that feel too silly to change everything in my code but having searched a lot throughout stack overflow and google for similar issues, I cannot seem to find out why.
There must be something wrong I cannot see in the way I have written the loop?
Use $x == $i to test for equality, not $x = $i, which is an assignment.
example:
foreach($boxes as $box) {
echo "$box \n";
}
Used to be fairly easy, I could just wrap the foreach around a check like:
if(is_array($boxes) && count($boxes) > 0) {
//foreach loop here
}
Without having to worry about a warning getting thrown if for whatever reason bad input was passed to the $boxes array.
When Iterators, were added to the mix, this no longer works, as Iteratable objects are not arrays. So, I have a few solutions, but am wondering if there is a 'best practice' for this.
// 1:
if($boxes instanceof Traversable && count($boxes) > 0) {
//foreach loop here
}
// 2:
if($boxes && count($boxes) > 0) {
//foreach loops here
}
There are others, but these seem like the most obvious. Anyone have any suggestions. PHP docs seem to be silent.
You shouldn't have the count($array) > 0 part, because a) foreach works fine with empty arrays, b) objects can be Traversable yet not be Countable and c) the value returned by count() may even (for objects) be disconnected from the number of items the traversal will yield.
And #1 there is different from #2; since $boxes instanceOf Traversable is not the same as $boxes. Also note that internally arrays don't implement Traversable.
I would go with
if (is_array($boxes) || $boxes instanceof Traversable) {
foreach (...)
}
This still doesn't guarantee that the traversal will be successful; the iteration may throw an exception at any point. In particular, for some classes it may not make sense to traverse them more than once.
I think generally in these cases you would probably know that the variable is going to be iterable if it is not null or false etc., so I would be happy to just do:
if ($boxes) {
foreach ($boxes as $box) {}
}
Maybe that is naive though
One possibility depending on your php version is a cast:
<?php
$a = array('foo', array('bar'));
foreach ($a as $thing)
foreach ((array) $thing as $item) // <-- here
echo "$item\n";
?>
This test will give true for both arrays and array-like objects
if (is_array($obj) || $obj instanceof Traversable) {
foreach ($obj as $item) { /* foreach loop is safe here */
}
}
In PHP5 you can iterate over any array or object so..
if (is_array($my_var) || is_object($my_var)) {
// Do some foreachin'
}
I'm finding myself doing a lot of things with associative arrays in PHP.
I was doing this:
foreach ($item as $key=>$value) {
if ($arr[$key] == null) {
$arr[$key] = 0;
}
$arr[$key] += $other_arr[$value];
}
But then I realised that it works fine if I exclude the line that initializes $arr[$key], presumably since it's null which is treated as the same as 0.
Is making that kind of assumption safe in php? And if it's safe, is it a good idea?
It is safe but I'd recommend against it.
If you put your error reporting up to E_NOTICES you'll see your code producing a lot of them, masking any real errors (such as a mistyped variable name).
What you should really be doing is:
if (!isset($arr[$key]))
$arr[$key] = 0;
This won't raise a notice (but be very careful not to mis-type $arr inside isset()).
As of php 7 you can now do the following:
foreach ($item as $key=>$value) {
$arr[$key] = ($arr[$key] ?? 0) + other_arr[$value];
}