Object comparison — == vs === for objects
Concept
PHP has two operators for comparing objects: == (loose equality) and === (strict identity). They have fundamentally different semantics and choosing the wrong one causes subtle, hard-to-debug bugs.
== (equality): Two objects are equal if they are of the same class AND all their property values are equal (using loose comparison). This is a deep structural comparison. Two different Point(1, 2) instances compare as equal with ==.
=== (identity): Two object references are identical only if they refer to the exact same object instance — same memory location, same object handle. A clone is never === to its original, even with identical property values.
Which to use: === is almost always the right choice for objects in conditional checks. "Is this the exact same object?" is the question you're usually asking. == is appropriate for value objects where two instances with the same data should be considered equal — but it's fragile because it uses loose comparison for property values.
Value objects and equality: For proper value equality, implement a custom equals() method that performs explicit strict comparison of properties. This is more reliable than relying on == because you control exactly what "equal" means.
Cloned objects: A clone always returns false with === (different handles). It may return true with == if all properties are equal.
Code Example
<?php
declare(strict_types=1);
class Point
{
public function __construct(
public float $x,
public float $y,
) {}
public function equals(self $other): bool
{
return $this->x === $other->x && $this->y === $other->y;
}
}
$p1 = new Point(1.0, 2.0);
$p2 = new Point(1.0, 2.0); // same values, different instance
$p3 = $p1; // same instance (handle copy)
$p4 = clone $p1; // new instance, same values
// == (equality — same class, same property values)
var_dump($p1 == $p2); // true — same class, same values
var_dump($p1 == $p4); // true — clone has same values
// === (identity — same object handle)
var_dump($p1 === $p3); // true — $p3 IS $p1
var_dump($p1 === $p2); // false — different instances
var_dump($p1 === $p4); // false — clone is a new instance
// Custom equals — explicit and clear
var_dump($p1->equals($p2)); // true
var_dump($p1->equals($p4)); // true
// The danger of == with objects containing objects
class Container
{
public function __construct(public Point $point) {}
}
$c1 = new Container(new Point(1.0, 2.0));
$c2 = new Container(new Point(1.0, 2.0));
var_dump($c1 == $c2); // true — recursive property comparison
// But this uses loose == for nested objects, which can cascade unexpectedly
// Practical: checking if an event listener is already registered
class EventEmitter
{
private array $listeners = [];
public function on(string $event, callable $fn): void
{
// Check if this exact callable is already registered
if (!in_array($fn, $this->listeners[$event] ?? [], strict: true)) {
$this->listeners[$event][] = $fn;
}
}
}