PHP Object Iteration and Iterators

Expose deliberate collection traversal instead of relying on object properties and choose a fresh iterator policy.

Lesson 53 of 86Advanced9 min read

PHP can iterate over an object's visible properties, but that is not always the collection interface you want. A basket object may contain a customer, configuration and a list of lines; foreach should expose the lines, not every public field. Iterator and IteratorAggregate let a class define intentional traversal behavior.

This guide compares the two interfaces, explains their relationship to Traversable, and builds a small collection that creates a fresh iterator for each pass. You will also learn why iterability does not imply array indexing or countability and why a reused generator can make a supposedly reusable collection one-pass.

Default object iteration follows visibility

When foreach iterates a normal object, it sees properties visible from the current scope. Public fields can be visible outside the class, while private fields are not a public collection API. Iteration from inside the class can see a different set. That makes property iteration unsuitable as an accidental permanent data contract.

Declaring public properties solely to make foreach work weakens encapsulation. Callers may then assign values that violate object invariants. Use a deliberate iteration interface when the object represents a collection. Keep metadata and internal storage separate from the elements exposed to callers.

The interface should also describe keys. They may be consecutive positions, stable identifiers or another documented mapping. A caller building JSON or a lookup map needs to know whether key preservation matters. Changing key meaning can break consumers even when the visible values remain the same.

Iterator owns traversal state

The Iterator interface defines current, key, next, rewind and valid. Together they describe a cursor: what value and key are at the current position, how to advance, how to return to the start, and whether the position contains a valid item. Foreach calls these methods according to the iteration protocol.

An object implementing Iterator owns mutable traversal state. Two loops over the same object can interfere if they advance the same cursor, especially when nested. That may be appropriate for a low-level stream, but a reusable collection often benefits from returning a separate iterator object for each traversal.

Use explicit return types compatible with modern interface declarations. Older examples sometimes omit them and now produce deprecation diagnostics for internal interface return types. Correct signatures improve compatibility and communicate the protocol; suppressing diagnostics should not be the default solution for code you control.

IteratorAggregate supplies an iterator

IteratorAggregate requires getIterator, which returns a Traversable object. The collection can return an ArrayIterator over stored values or a fresh Generator created from a generator function. This separates the collection's stored data from one traversal's cursor state.

Arrays are iterable, but getIterator's Traversable return contract requires an appropriate object, not simply the raw array. Wrap an array in ArrayIterator or yield its contents. Traversable itself is the marker interface behind Iterator and IteratorAggregate; userland classes implement one of those concrete iteration contracts rather than using it alone.

A fresh iterator supports repeated or nested traversal when the source itself permits it. Returning the same already consumed Generator does not provide that guarantee. State your repeatability policy and keep external stream lifetime visible when traversal owns files, network responses or database cursors.

Iteration, counting and mutation are separate contracts

An iterable object is not automatically Countable and does not necessarily support ArrayAccess. Implement those interfaces only when their semantics are useful and clear. Count may be cheap for an in-memory list but expensive or destructive for a streaming source. Avoid promising a cheap count when it requires consuming every item.

Decide whether callers may mutate the collection while iterating. A snapshot iterator can provide stable values, while a live iterator may observe changes. Objects stored as elements can still be mutable even if the collection's structure is protected. A copied array does not deep-clone the contained objects.

The PHP object iteration guide, Iterator reference and IteratorAggregate reference document the protocols. Choose interfaces from actual caller needs instead of implementing every collection-related interface for completeness.

PHP Object Iteration and Iterators Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Observe a normal object's public property

The private field is not visible to this external loop. That protects ordinary access but does not mean the public field was intentionally designed as a collection. Avoid placing secrets in public fields, and avoid treating visibility-based iteration as a secure serialization policy.

PHP
<?php
class SettingsView {
    public string $theme = 'light';
    private string $secret = 'internal';
}
foreach (new SettingsView() as $key => $value) {
    echo $key . ':' . $value . PHP_EOL;
}
Output
theme:light
Example 2

Return a fresh array iterator

Each getIterator call creates a new cursor over the stored array. The example's element contract is strings, but the array declaration alone does not enforce that. Validate construction input when a collection must guarantee one element type.

PHP
<?php
class Labels implements IteratorAggregate {
    public function __construct(private array $labels) {}

    public function getIterator(): Traversable {
        return new ArrayIterator($this->labels);
    }
}
$labels = new Labels(['Pen', 'Book']);
foreach ($labels as $label) {
    echo $label . PHP_EOL;
}
Output
Pen
Book

Common PHP Object Iteration and Iterators Mistakes

Accidentally exposing implementation storage

Do not return a mutable internal object as an iterator if callers can use it to change storage you intended to protect. Understand whether the iterator shares state or represents a snapshot. Similarly, avoid relying on a public array property and hoping callers treat it as read-only.

Do not implement a complex manual Iterator when IteratorAggregate with a small generator describes the same traversal clearly. Manual cursor logic introduces positions, boundary conditions and reset behavior that all need to be correct. Choose the simplest protocol that fulfills the required collection contract.

PHP Object Iteration and Iterators in Real Projects

Practical task: repeatable keyed stock collection

Create a stock collection that exposes a map from product IDs to counts. Use IteratorAggregate and a fresh generator on every call. Then traverse the collection twice, printing the same mapping each time. Preserve identifiers because they carry business meaning, not display position.

PHP
<?php
class StockMap implements IteratorAggregate {
    public function __construct(private array $stock) {}

    public function getIterator(): Traversable {
        foreach ($this->stock as $id => $count) {
            yield $id => $count;
        }
    }
}
$stock = new StockMap(['pen' => 3, 'book' => 1]);
for ($pass = 1; $pass <= 2; $pass++) {
    foreach ($stock as $id => $count) {
        echo $pass . ':' . $id . '=' . $count . PHP_EOL;
    }
}
Output
1:pen=3
1:book=1
2:pen=3
2:book=1

GetIterator is a generator method, so each call creates a fresh Generator. The collection itself remains reusable. Try storing one generator in a property and returning it every time, then consider why the second pass no longer has the same contract. A production stock collection should validate keys and nonnegative integer counts during construction.

PHP Object Iteration and Iterators FAQ

Can getIterator return an array?

Its Traversable contract expects an appropriate object. Use ArrayIterator or a generator to expose an array's elements.

Does foreach require Countable?

No. Iteration and countability are separate capabilities.

Which interface is easier for stored collections?

IteratorAggregate often keeps cursor state separate and can provide fresh traversals. Iterator fits objects that intentionally are cursors.

Can keys be strings?

Yes. Document whether they are identifiers and preserve them when that meaning matters.

Does a fresh iterator clone every element?

No. Referenced objects can still be shared and mutable. Deep-copy or immutable-value requirements need separate design.

PHP Object Iteration and Iterators Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

A collection needs foreach but not indexing. Which capability should you implement deliberately?

PHP Object Iteration and Iterators Quiz

2 questions to check what you learned.

  1. Which method does IteratorAggregate require?

    • getAllProperties()
    • getIterator()
    • __toArray()
  2. What can happen if a reusable collection always returns the same consumed Generator?

    • It automatically clones all values
    • It becomes Countable automatically
    • Later traversals can fail or be exhausted