In PHP, how do == and === behave when comparing two objects, and what traps hide in loose object comparison?
answer
- identity versus same class and state
- same instance only for ===
- properties compared loosely, recursively
- different classes are never ==
- cycles throw Nesting level too deep
basics
~20 s=== on objects is true only for the same instance. == is true when both are instances of exactly the same class and every property compares equal with ==, recursively, so loose juggling applies to nested values.
solid answer
~50 s`===` is **identity**: two object operands are identical only if they are the same instance, which is the same as holding the same handle. `==` is **structural and loose**: it is true when both objects are instances of *exactly the same class* and each declared and dynamic property compares equal using `==` again, so nested objects are compared recursively and scalar properties go through type juggling (`'1e1' == '10'` is true, `null == false` is true). A subclass instance is never `==` to a parent-class instance with the same data. Comparing two distinct object graphs that contain cycles can throw an `Error` with the message `Nesting level too deep - recursive dependency?`. Internal classes can install their own comparison: `DateTime` objects compare by the instant they represent. For domain equality, write an explicit `equals()` method rather than relying on `==`.
code
php · 31 lines<?php
declare(strict_types=1);
final class Sku
{
public function __construct(public string $code) {}
}
$a = new Sku('1e1');
$b = new Sku('10');
$c = $a;
var_dump($a == $b); // bool(true): numeric strings compare as numbers
var_dump($a === $b); // bool(false): two instances
var_dump($a === $c); // bool(true): one instance, two handles
final class Node
{
public ?Node $next = null;
}
$x = new Node();
$x->next = $x;
$y = new Node();
$y->next = $y;
try {
var_dump($x == $y);
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL; // Nesting level too deep - recursive dependency?
}go deeper
Recall that === means the same instance and == means same class with equal properties.
Explain that == recurses with loose comparison, so juggling applies inside objects, and that differing classes are never equal.
Point out the production traps: cyclic graphs throwing Error, slow deep walks, in_array defaulting to loose comparison, and the case for explicit equals() methods.
Frame object equality as an API decision: which types are values with explicit equality and which are entities compared by identity.
## Two different questions Comparing objects asks one of two questions: - **Identity** — are these the same object? In PHP that is `===` (and `!==`). - **Equality** — do these objects hold equivalent state? PHP's built-in answer is `==` (and `!=`), but its definition is mechanical and loose. An object variable holds a **handle** to an instance. `$b = $a` copies the handle, so `$a === $b` is true. `clone $a` creates a new instance, so `clone $a === $a` is false even though every property matches. ## How == compares two objects For two plain user objects, the engine's standard comparison does this: 1. If both operands are the **same instance**, they are equal. 2. If they are instances of **different classes**, they are not equal — this includes a subclass versus its parent, even with identical data. 3. Otherwise it walks the properties and compares each pair with `==`. The first unequal pair makes the result unequal. A property that is initialized on one side and uninitialized on the other also makes them unequal. Because step 3 uses loose `==`, every rule of type juggling applies inside the object: | Property values | Result of `==` on the objects | |---|---| | `10` and `'10'` | equal | | `'1e1'` and `'10'` (numeric strings) | equal — both compare as numbers | | `null` and `false` | equal | | `0` and `'abc'` | not equal since PHP 8.0 | | two different nested objects with the same state | equal — compared recursively | So two `Sku` objects wrapping the codes `'1e1'` and `'10'` compare `==` true, which is exactly the kind of bug a product-code comparison should not have. ## Recursion and cycles Recursion into nested objects is what makes `==` convenient and also what makes it dangerous on object graphs. If each object refers back to itself or to its parent (a tree node with a `parent` property, a doubly linked list, an ORM entity graph), the comparison would loop forever. When the walk re-enters an object it is already comparing, the engine throws an `Error` with the message `Nesting level too deep - recursive dependency?`. It is an `Error`, not an `Exception`, so `catch (Exception $e)` does not catch it. The same deep walk also makes `==` on large graphs slow. ## Classes that define their own comparison Internal classes can override the comparison handler, and the manual notes that extensions may define their own `==` rules. The everyday example is `DateTime` and `DateTimeImmutable`: they compare by the **instant** they represent (seconds and microseconds), so two objects showing different time zones but the same moment are `==`, and `<` and `>` work chronologically. User classes cannot overload `==`. ## What to use in practice - Use `===` when you mean "the same object" — checking whether an entity is already in an identity map, or whether a listener is the one you registered. - Do not use `==` as domain equality. It is tied to the class's private layout, compares loosely, and breaks when a cached or lazily loaded property differs. - Write an explicit `equals(self $other): bool` on value objects that compares the fields that define the value, with `===`. - `in_array($obj, $list)` and `array_search()` use `==` by default; pass `true` as the strict argument to compare by identity. ## Writing equality you control A value object that needs equality should state it: 1. Decide which fields define the value (for a `Money`: amount and currency, not a cached formatted string). 2. Compare those fields with `===`, and delegate to the fields' own `equals()` for nested value objects. 3. Accept only the same type (`self $other`), so a cross-type comparison is a type error rather than a silent `false`. This keeps equality stable when a private cache or a lazily filled property is added later, and it makes the rule visible to reviewers and static analysis. Entities with an identity, such as a stored quote with an id, usually compare ids rather than state. ## Summary `===` answers "same instance"; `==` answers "same class and loosely equal properties, recursively". The second is rarely the equality your domain means, which is why most codebases compare objects with `===` or an explicit method.
- Does `in_array($quote, $quotes)` find an object by identity?Not by default. `in_array()` and `array_search()` compare with `==` unless their third `$strict` argument is `true`, so an equal-looking copy matches and a cyclic graph can throw. Pass `true` to compare with `===`, which matches only the same instance.
- Why is a subclass instance never `==` to a parent instance holding the same data?The standard handler first checks that both objects have exactly the same class; if not, they are uncomparable and `==` returns false before any property is looked at. Equality across a hierarchy has to be defined explicitly in an `equals()` method.
saying these in an interview costs you the question
- == on objects checks whether both variables point to the same instance.
- === on objects compares every property with strict comparison.
- An instance of a subclass is == to a parent instance with the same properties.
- Nested properties are compared with === when objects are compared with ==.
- Comparing cyclic object graphs with == just returns false.