0

Object comparison — == vs === for objects

Intermediate5 min read·php-07-015
interview

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
<?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;
        }
    }
}