Skip to content

The PHP CachingIterator

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";
}

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-&gt;getCache() as $offset=&gt;$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

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>

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

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";
}

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

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.

  1. The ‘cached’ version of the iterator should be the one that does NOT change. The iterator itself should reflect the changes made.
  2. Calling rewind() should never cause the cache to forget everything except the last element.
  3. 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=