Dear Reader,
(Sample code for those too dang lazy to cut ‘n paste)
How I got here
In the course of writing my next book, “Iterating PHP Iterators”, I found something very interesting.
I have a short chapter on the CachingIterator. One of the flags in the CachingIterator is FULL_CACHE. It was during my experiments with tha, that I found…an anomaly.
Note: As of yet, I have not reported this as a bug in PHP because it may just be a situation of “I’m doing it wrong”. I’m putting this out here mainly so someone can point me in the right direction. If no one can, then I’ll file a bug.
The proof of error code
The example I am using in my book is the 7 Dwarfs. Here is the code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <?php
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
} |
<?php
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
}
That code actually works, even if it doesn’t work the way I would expect it to. I would expect that iterating over $it
would give me the modified version, not the original “cached” version. Note that Bashful
is still in the list and Kathy
is not. It is the original list as we loaded it into the ArrayIterator
. Also, line 11 is very important, if a bit silly. Yes, you have to spin through the entire array if you pass it in on the constructor, otherwise, the cache doesn’t get loaded.
Now let’s add a little more to it.
1
2
3
| foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
} |
foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
}
This now outputs:
$ php ../examples/test.php
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc
Cache: 1 == Grumpy
Cache: 2 == Happy
Cache: 3 == Sleepy
Cache: 4 == Bashful
Cache: 5 == Sneezy
Cache: 6 == Dopey
Cache: 7 == Doc |
$ php ../examples/test.php
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc
Cache: 1 == Grumpy
Cache: 2 == Happy
Cache: 3 == Sleepy
Cache: 4 == Bashful
Cache: 5 == Sneezy
Cache: 6 == Dopey
Cache: 7 == Doc
Ok, so now, even when we pull the cache, we still get the original list. I’m not sure how that is right, ever. I know a few of you are saying “but Cal, you have to rewind().” It is to those of you who I say “read my book”. :) But just for grins and giggles, let’s rewind the iterator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| <?php
$it = null;
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
}
$it->rewind();
foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
}
</code> |
<?php
$it = null;
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
}
$it->rewind();
foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
}
</code>
Now when we run it we get this:
$ php ../examples/test.php
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc
Cache: 1 == Grumpy |
$ php ../examples/test.php
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc
Cache: 1 == Grumpy
Hmmm…well that ain’t right.
Here is what DID work. I am not entirely sure why at this point, I’m still investigating.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| <?php
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
}
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
} |
<?php
$dwarves = [1=>'Grumpy',
2=>'Happy',
3=>'Sleepy',
4=>'Bashful',
5=>'Sneezy',
6=>'Dopey',
7=>'Doc'];
$it = new CachingIterator(new ArrayIterator($dwarves),
CachingIterator::FULL_CACHE);
foreach($it as $v);
$it->offsetUnset(4);
$it->offsetSet('Cal','Kathy');
$it[5]='Surly';
foreach($it->getCache() as $offset=>$value) {
echo 'Cache: '.$offset.' == '.$value."\n";
}
foreach($it as $offset=>$value) {
echo 'Original: '.$offset.' == '.$value."\n";
}
Now we are through the looking glass. The order in which the loops appear in your code makes a difference? Technically, this code outputs the list correctly if you ignore the fact that the cache version should be the immutable one and that $it itself should reflect the changes.
$ php ../examples/test.php
Cache: 1 == Grumpy
Cache: 2 == Happy
Cache: 3 == Sleepy
Cache: 5 == Surly
Cache: 6 == Dopey
Cache: 7 == Doc
Cache: Cal == Kathy
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc |
$ php ../examples/test.php
Cache: 1 == Grumpy
Cache: 2 == Happy
Cache: 3 == Sleepy
Cache: 5 == Surly
Cache: 6 == Dopey
Cache: 7 == Doc
Cache: Cal == Kathy
Original: 1 == Grumpy
Original: 2 == Happy
Original: 3 == Sleepy
Original: 4 == Bashful
Original: 5 == Sneezy
Original: 6 == Dopey
Original: 7 == Doc
BONUS ROUND:
Take the above code, now swap the two foreach
statements. See what I mean? The order that the foreach
statements are executed in should have absolutely no effect on the output. If this is expected behavior then we kinda need to put it in the manual.
Sooooo…TIL. don’t use the FULL_CACHE
flag on the CachingIterator
. I am not sure what the FULL_CACHE
flag is supposed to do, but it doesn’t seem to do anything useful at the moment.
Summary:
So today I learned, don’t use the FULL_CACHE
flag on the CachingIterator
. I am not sure what the FULL_CACHE
flag is supposed to do, but it doesn’t seem to do anything useful at the moment. Also, it can screw things up for you.
Here are 3 takeaways.
- The ‘cached’ version of the iterator should be the one that does NOT change. The iterator itself should reflect the changes made.
- Calling
rewind()
should never cause the cache to forget everything except the last element.
- If you pass in the ArrayIterator in the constructor, it does not get loaded into the cache, you have to put an empty
foreach
loop in your code to load the cache.
I hope this helps someone along the way.
Until next time,
I <3 |<
=C=