Object Comparison & Iteration
Objects behave differently from scalars in two areas that trip people up constantly: comparing them, and looping over them. Comparing two objects with == versus === asks two genuinely different questions, and looping over an object with foreach only works "for free" for plain public properties — anything more deliberate requires implementing an interface that tells PHP how to walk your object, or how to count it. This page covers both: == versus ===, the Iterator and IteratorAggregate interfaces, and the Countable interface.
== compares values, === compares identity
== between two objects asks "are these the same class, with equal values in every property?" — it compares structurally, recursively, without caring whether it's looking at one object or two. === asks a completely different question: "are these two variables pointing at the exact same object instance in memory?" A === comparison is only true when both sides refer to the one and same object — the way $a === $a is always true for any object $a.
Loose (==) vs strict (===) comparison
<?php
class Point {
public function __construct(public int $x, public int $y) {}
}
$a = new Point(1, 2);
$b = new Point(1, 2); // same class, same values, different instance
$c = $a; // same instance as $a
var_dump($a == $b); // true: same class, equal properties
var_dump($a === $b); // false: two distinct objects
var_dump($a === $c); // true: literally the same objectbool(true) bool(false) bool(true)
$a and $b are two separate Point objects that happen to hold identical coordinates, so == treats them as equal — but === correctly reports them as different instances. $c is not a new point at all; it's the same handle as $a (as covered on the object-cloning page), so === reports true.
== requires the same class
== is not a duck-typing comparison — two objects of unrelated classes are never == equal to each other, even if their public properties happen to line up exactly.
Different classes are never == equal, regardless of property values
<?php
class Point {
public function __construct(public int $x, public int $y) {}
}
class Coordinate {
public function __construct(public int $x, public int $y) {}
}
$point = new Point(3, 4);
$coord = new Coordinate(3, 4);
var_dump($point == $coord);bool(false)
Even though every property matches, Point and Coordinate are different classes, so == short-circuits to false before it even looks at the properties.
Making an object iterable: Iterator
Looping over an object with foreach works automatically only for its accessible public properties, which is rarely what you actually want from a purpose-built collection class. To take full control of what foreach sees, implement the built-in Iterator interface, which requires five methods: current(), key(), next(), rewind(), and valid(). PHP calls these in a well-defined sequence every time your object is the subject of a foreach loop.
A playlist class that controls its own iteration
<?php
class Playlist implements Iterator {
private array $tracks;
private int $position = 0;
public function __construct(array $tracks) {
$this->tracks = $tracks;
}
public function current(): mixed {
return $this->tracks[$this->position];
}
public function key(): mixed {
return $this->position;
}
public function next(): void {
$this->position++;
}
public function rewind(): void {
$this->position = 0;
}
public function valid(): bool {
return isset($this->tracks[$this->position]);
}
}
$playlist = new Playlist(['Intro', 'Chapter One', 'Chapter Two']);
foreach ($playlist as $index => $track) {
echo "{$index}: {$track}\n";
}0: Intro 1: Chapter One 2: Chapter Two
foreach calls rewind() once at the start, then repeatedly checks valid(), reads current() and key(), and calls next() until valid() returns false. Because all of this logic lives inside Playlist, you're free to change the internal storage — a database cursor, a lazily generated sequence, anything — without changing how callers loop over it.
The simpler route: IteratorAggregate
Implementing all five Iterator methods by hand is often more ceremony than a simple collection needs. IteratorAggregate requires just one method, getIterator(), which returns anything already iterable — commonly an ArrayIterator wrapping an internal array. PHP then drives that returned iterator on your object's behalf.
The same playlist, built on IteratorAggregate instead
<?php
class Playlist implements IteratorAggregate {
public function __construct(private array $tracks) {}
public function getIterator(): Iterator {
return new ArrayIterator($this->tracks);
}
}
$playlist = new Playlist(['Intro', 'Chapter One', 'Chapter Two']);
foreach ($playlist as $index => $track) {
echo "{$index}: {$track}\n";
}0: Intro 1: Chapter One 2: Chapter Two
This produces identical foreach behavior to the hand-written Iterator version above, with far less code. Reach for Iterator directly only when you need custom step-by-step control — for example, skipping certain elements, or generating values on demand instead of pulling them from an already-built array.
Making count() work: Countable
Calling PHP's built-in count() function on a plain object either raises a warning or returns 1, depending on the PHP version and settings — neither is useful. Implementing the Countable interface, which requires a single count(): int method, tells count() exactly how to size up your object.
A playlist that reports its own size to count()
<?php
class Playlist implements IteratorAggregate, Countable {
public function __construct(private array $tracks) {}
public function getIterator(): Iterator {
return new ArrayIterator($this->tracks);
}
public function count(): int {
return count($this->tracks);
}
}
$playlist = new Playlist(['Intro', 'Chapter One', 'Chapter Two']);
echo count($playlist), "\n";
echo 'Track count: ' . count($playlist) . "\n";3 Track count: 3
Whenever count($playlist) runs, PHP notices that Playlist implements Countable and calls its count() method instead of trying to count public properties. A single class can combine IteratorAggregate and Countable (and often ArrayAccess too), so it behaves like a native array in every situation that matters — foreach, count(), and $obj[$key] — while still enforcing whatever validation or lazy-loading logic the class needs internally.
==on two objects means "same class and equal properties" — a structural comparison, not an identity check.===on two objects means "literally the same instance" — true only when both variables reference one object.Objects of different classes are never
==equal, no matter how similar their properties are.Implement
Iterator(five methods) for full manual control overforeach, orIteratorAggregate(one method) to delegate to an existing iterable likeArrayIterator.Implement
Countable'scount(): intmethod so the built-incount()function works correctly on your object.