30

Is it possible to "peek ahead" while iterating an array in PHP 5.2? For example, I often use foreach to manipulate data from an array:

foreach($array as $object) {
  // do something
}

But I often need to peek at the next element while going through the array. I know I could use a for loop and reference the next item by it's index ($array[$i+1]), but it wouldn't work for associative arrays. Is there any elegant solution for my problem, perhaps involving SPL?

hakre
  • 193,403
  • 52
  • 435
  • 836
pako
  • 1,908
  • 4
  • 24
  • 40

6 Answers6

58

You can use the CachingIterator for this purpose.

Here is an example:

$collection = new CachingIterator(
                  new ArrayIterator(
                      array('Cat', 'Dog', 'Elephant', 'Tiger', 'Shark')));

The CachingIterator is always one step behind the inner iterator:

var_dump( $collection->current() ); // null
var_dump( $collection->getInnerIterator()->current() ); // Cat

Thus, when you do foreach over $collection, the current element of the inner ArrayIterator will be the next element already, allowing you to peek into it:

foreach($collection as $animal) {
     echo "Current: $animal";
     if($collection->hasNext()) {
         echo " - Next:" . $collection->getInnerIterator()->current();
     }
     echo PHP_EOL;
 }

Will output:

Current: Cat - Next:Dog
Current: Dog - Next:Elephant
Current: Elephant - Next:Tiger
Current: Tiger - Next:Shark
Current: Shark

For some reason I cannot explain, the CachingIterator will always try to convert the current element to string. If you want to iterate over an object collection and need to access properties an methods, pass CachingIterator::TOSTRING_USE_CURRENT as the second param to the constructor.


On a sidenote, the CachingIterator gets it's name from the ability to cache all the results it has iterated over so far. For this to work, you have to instantiate it with CachingIterator::FULL_CACHE and then you can fetch the cached results with getCache().

Gordon
  • 312,688
  • 75
  • 539
  • 559
  • 1
    +1 Didn't even know these ([x]Iterators) existed, very useful, espec the DirectoryIterator. That's going to save me a bulk load of work next time I'm doing file stuff. Thanks :) – Psytronic Mar 16 '10 at 21:58
  • 2
    @Psytronic they are really neat. The ability to stack them allows for very cool and flexible stuff. Unfortunately, there are poorly documented, but have a look at http://www.phpro.org/tutorials/Introduction-to-SPL.html – Gordon Mar 16 '10 at 22:03
  • Unfortunately, the solution doesn't work if the array contains objects and not strings. I get the following exception: `Catchable fatal error: Object of class MySampleClass could not be converted to string in /home/www/test.php on line 398` – pako Mar 16 '10 at 23:02
  • 2
    @pako Either implement the `__toString` method in `MySampleClass` or pass `CachingIterator::TOSTRING_USE_CURRENT` as the second param in the CachingIterator constructor. – Gordon Mar 16 '10 at 23:27
  • Okay I will try that. But why does the iterator need this step at all? I hope it doesn't change the order of the elements in the array based on their string representations. – pako Mar 17 '10 at 11:37
  • 2
    @pako I don't know why it needs this step, but apparently it does. The Iterators are unfortunately poorly documented at the moment. The solution I gave is based on using PHP's Reflection API and Trial and Error. If you are concerned the Iterator will do something it shouldn't do, make sure with a UnitTest. – Gordon Mar 17 '10 at 11:55
20

Use array_keys.

$keys = array_keys($array);
for ($i = 0; $i < count($keys); $i++) {
    $cur = $array[$keys[$i]];
    $next = $array[$keys[$i+1]];
}
Bart van Heukelom
  • 43,244
  • 59
  • 186
  • 301
  • Why would I take on all the complication of the accepted answer to do such a basic thing, when the answer is right there in the core functions? Thank you. – Noumenon Mar 06 '13 at 18:36
  • I like this answer, there's lots of advantages to being able to refer to items numerically. – Kelly Larsen Jun 01 '14 at 23:22
8

You can use next and prev to iterate an array. current returns the current items value and key the current key.

So you could do something like this:

while (key($array) !== null) {
    next($array); // set pointer to next element
    if (key($array) === null) {
        // end of array
    } else {
        $nextItem = current($array);
    }
    prev($array); // resetting the pointer to the current element

    // …

    next($array);
}
Hannes Schneidermayer
  • 4,729
  • 2
  • 28
  • 32
Gumbo
  • 643,351
  • 109
  • 780
  • 844
  • 3
    The code looks pretty complicated, and it's prone to error (too many "nexts/prevs" and very strange things can happen...). – pako Mar 16 '10 at 22:02
4

I know that this is an old post, but I can explain that current/next/prev thing better now. Example:

$array = array(1,2,3,2,5);

foreach($array as $k => $v) {
    // in foreach when looping the key() and current() 
    // is already pointing to the next record
    // And now we can print current
    print 'current key: '.$k.' and value: '.$v;
    // if we have next we can print its information too (key+value)
    if(current($array)) {
         print ' - next key: '.key($array).' and value: '.current($array);
         // at the end we must move pointer to next
         next($array);
    }
    print '<br>';
}

// prints:
// current key: 0 and value: 1 - next key: 1 and value: 2
// current key: 1 and value: 2 - next key: 2 and value: 3
// current key: 2 and value: 3 - next key: 3 and value: 2
// current key: 3 and value: 2 - next key: 4 and value: 5
// current key: 4 and value: 5
Asciiom
  • 9,867
  • 7
  • 38
  • 57
Juhani
  • 41
  • 2
0

I know I could use a for loop and reference the next item by its index ($array[$i+1]), but it wouldn't work for associative arrays.

Consider converting your associative array into an sequentially indexed one with array_values(), allowing you to use the simple for loop solution.

Noumenon
  • 5,099
  • 4
  • 53
  • 73
0

Old post but my two cents:

If you are trying to peek ahead, you really need to ask yourself "Am I solving this problem the best way possible."

You can solve all peek-ahead problems without ever doing a peek-ahead. All you need is a "$prevItem" reference declared before the collection and initialize it as null. Each time you go through the loop, at the end, set $prevItem to the current array item you just evaluated. Effectively, instead of peaking ahead, you start executing your real logic at the second item and use the $prevItem reference to do your operation. You skip the first item by noting that $prevItem is null.

$prevItem = null;
$prevKey = null;

foreach($collection as $key => $val)
{
     if($prevItem != null)
     {
          //do your operation here
     }


     $prevItem = $val;
     $prevKey = $key;
}

It's clean code and its a common pattern.

Stay away from poking around at underlying data structures while you are iterating through them... its never good practice, and extremely rare that you would need to do it.

JamesHoux
  • 2,999
  • 3
  • 32
  • 50
  • You're right in most cases, but I'm actually working on an application now where I need to display information about the next iteration of the loop while still in the iteration before it. I grant it's a unique situation, but such situations do exist. – Matthew Oct 06 '20 at 00:31
  • Please provide example of your situation. There are few situations where you would be unable to refactor a loop to start one cycle ahead and look at previous data. I haven't even seen such a situation that didn't prove itself to be a weak design. – JamesHoux Oct 06 '20 at 18:08
  • I don't have a succinct code sample to paste in here, but I'm working on a platform to optimize appliance delivery routes. In the list of a day's stops, we need to provide some information about Stop B intermingled with information in the Stop A iteration of the loop. It's the "intermingled" part that makes this tricky; otherwise your $prevItem approach would work as well here as in the many other contexts I've used it in before. – Matthew Oct 07 '20 at 15:29