foreach over an object walks its public properties unless the class implements Traversable through one of two children. Iterator makes the object its own cursor with five methods; IteratorAggregate has one, getIterator(), which returns any iterator, most simply a generator (Generators and yield). The traced class prints the order in which foreach calls the five.
<?php
final class TracedLines implements Iterator {
private int $i = 0;
public function __construct(private array $lines) {}
public function rewind(): void { echo 'rewind '; $this->i = 0; }
public function valid(): bool { echo 'valid '; return $this->i < count($this->lines); }
public function current(): mixed { echo 'current '; return $this->lines[$this->i]; }
public function key(): mixed { echo 'key '; return "L{$this->i}"; }
public function next(): void { echo "next\n"; $this->i++; }
}
foreach (new TracedLines(['BK-PHP-01', 'BK-SQL-01']) as $k => $sku) echo "=> $k $sku, ";
final class Order implements IteratorAggregate {
public function __construct(private array $lines) {}
public function getIterator(): Generator {
foreach ($this->lines as $sku => [$qty, $cents]) yield $sku => $qty * $cents;
}
}
$order = new Order(['BK-PHP-01' => [2, 3990], 'BK-SQL-01' => [1, 4450]]);
echo "(end)\n", json_encode(iterator_to_array($order)), "\n";Output
rewind valid current key => L0 BK-PHP-01, next
valid current key => L1 BK-SQL-01, next
valid (end)
{"BK-PHP-01":7980,"BK-SQL-01":4450}Each pass runs valid(), current(), key(), the loop body and next(), and a valid() that returns false ends the loop. Prefer IteratorAggregate: it is shorter, and each foreach gets a fresh iterator, so nested loops over one order do not share a position.