Object Comparison in PHP: == vs. === and What Is Really Compared
AI generated
8.4
PHP
Object Comparison in PHP
== compares values, === compares identity: what that means for your code

With scalars the difference between == and === is already tricky enough, but with objects an entirely separate dimension appears that many developers underestimate. == compares class and every property recursively for objects, while === checks only whether both operands refer to the exact same instance in memory. PHP offers no magic method like __equals to customize this behavior. This article explains both operators precisely and shows how to build predictable, custom comparison logic for value objects.

12 min read Object Comparison Value Objects

1. Two fundamentally different comparisons for objects

For objects, == and === differ far more fundamentally in PHP than for any other type. While both operators merely differ in how strictly they handle type conversion for scalars, with objects they answer entirely different questions: == asks whether two objects are equal in content, === asks whether they are exactly the same instance. Anyone unaware of this distinction ends up writing comparisons that behave completely differently from what was intended.

This distinction is not a PHP peculiarity without precedent, it mirrors a well known concept from other languages: value equality versus reference identity. The decisive difference from languages like Java or C Sharp, however, is that PHP offers no overridable method for value equality, the behavior of == with objects is hardwired into the language and cannot be customized per class.

2. How == actually works with objects

The == operator returns true for two objects exactly when both are instances of the same class and all of their properties, compared with ==, are also equal. The comparison happens recursively: if a property itself holds an object, that object is compared again by the same rules. Two objects of different classes are never equal with ==, even if they carry identical properties with identical values.

This rule has an important consequence: two separately created instances of a class with the same constructor values are equal with ==, even though they are two completely independent objects in memory. For simple, immutable data containers this is often exactly the desired behavior, but for more complex objects with resource handles, timestamps, or circular references, the recursive comparison can become unexpectedly expensive or even faulty in edge cases.


<?php

declare(strict_types=1);

final class Point
{
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {
    }
}

$a = new Point(1.0, 2.0);
$b = new Point(1.0, 2.0);

var_dump($a == $b);   // true, same class and same property values
var_dump($a === $b);  // false, two distinct instances

3. How === checks real identity

The === operator checks, for objects, whether both operands point to the exact same internal object handle. Internally PHP tracks every object through a unique handle reference, independent of any variable that may currently reference it. Assigning an object to a new variable through a simple assignment does not create a copy, it merely creates another reference to the same handle, which is why === returns true in that case.

This exact behavior makes === the right tool when identity, not equivalence, is actually the question, for example when checking whether an event listener has already been registered, or when removing a specific object from a collection. In both cases what matters is whether it is exactly the same object, not whether an equivalent object exists.

4. Recursion in nested objects and its pitfalls

The recursive nature of == quickly becomes a trap with deeply nested object structures. If an object contains a property holding a collection of further objects, for example an order with multiple line items, == recursively compares every single item against the corresponding item of the other object. For large object graphs this can quickly become expensive and in many cases is not the desired behavior functionally either.

It gets even more problematic with circular references, where two objects reference each other. Comparing such objects with == can end up in an infinite loop, or at least produce a confusing failure that is hard to trace. In practice, == should therefore only be used deliberately on manageable, shallow structures for objects, not as a generic default for arbitrary object comparisons.

5. Why PHP has no __equals

Unlike Java, where every class implicitly inherits from Object and can override equals, PHP offers no comparable magic method. There is no way to specifically define the behavior of == for a custom class. The only exception involves SPL classes that implement an ArrayAccess or Comparable like interface, but even those do not change the fundamental behavior of == or === at all.

This is a deliberate language decision: it prevents hidden, surprising side effects on the comparison operator, as occasionally happens in other languages when a poorly implemented equals method is inconsistent with hashCode. The price is that every class with functionally relevant equality logic must offer it explicitly as its own method, instead of relying on the operators.

6. Custom comparison logic without operator overloading

The established convention in the PHP community is an explicit equals method that precisely defines which properties matter for equality and how they are compared. This makes the comparison logic self documenting and readable, instead of relying on the implicit, recursive behavior of ==, which quickly becomes unpredictable for more complex classes.

An additional benefit of a custom equals method is the ability to deliberately include only functionally relevant properties. An object may carry internal, technical properties, such as a cache value or a creation timestamp, that are irrelevant to functional equality but would still skew an == comparison, since == unconditionally includes every property.

7. Practical example: comparing a value object

A monetary amount illustrates the difference well. A Money value object should consider two amounts equal exactly when amount and currency match, regardless of whether it is the same instance. An explicit equals method makes this rule clear and testable, while == delivers the same result in the simple case but can easily produce wrong results once additional, functionally irrelevant properties are added.

This method can then be used in collections, sorting, or duplicate checks, without anyone needing to rely on the implicit behavior of ==. Especially in larger codebases with many value objects, this consistency pays off, because comparison behavior is defined in a single, clearly visible place instead of being implicitly derived from the class's property structure.


<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        private readonly int $amountInCents,
        private readonly string $currency,
        private readonly DateTimeImmutable $createdAt,
    ) {
    }

    /**
     * Compares only the functionally relevant values, ignores createdAt.
     */
    public function equals(self $other): bool
    {
        return $this->amountInCents === $other->amountInCents
            && $this->currency === $other->currency;
    }
}

$a = new Money(1999, 'EUR', new DateTimeImmutable('2026-01-01'));
$b = new Money(1999, 'EUR', new DateTimeImmutable('2026-08-08'));

var_dump($a->equals($b)); // true, equal in business terms
var_dump($a == $b);       // false, createdAt differs

8. Proving identity with spl_object_id

Sometimes object identity needs to be not just checked but used as a unique key, for example in a map whose keys are object instances. Since PHP arrays do not accept objects as keys, spl_object_id helps by producing a unique integer that stays stable for the lifetime of the object. Before PHP 7.2, spl_object_hash was the common alternative, spl_object_id is more performant today and should be preferred.

One important caveat applies: the ID returned by spl_object_id can be reused for a new object once the original is destroyed by the garbage collector. As long as a reference to the original object is actively kept, the ID stays unique, but without that guarantee you should not rely on the ID alone, and instead use the object itself as a key in an SplObjectStorage, which was designed exactly for this purpose.


<?php

declare(strict_types=1);

$storage = new SplObjectStorage();

$order = new stdClass();
$storage[$order] = 'processed';

var_dump($storage->contains($order)); // true
var_dump(spl_object_id($order));       // e.g. int(1), stable while $order lives

9. Rules of thumb for everyday practice

A good baseline: use === for identity checks, such as removing a specific object from a list or checking whether a callback is already registered. Use a custom equals method for functional equality, especially with value objects where two independent instances holding the same values should count as equal. Using == on objects should be deliberately avoided, except for simple, shallow data containers with no irrelevant properties.

This clear separation avoids an entire class of subtle bugs that typically only surface once additional properties are added or nesting gets deeper. Consistently distinguishing identity, functional equality, and the implicit recursive == semantics from the start avoids surprises that would otherwise only show up through failing tests in later project phases.

Operator/Technique What is compared Recursive? Typical use
== Class plus every property Yes Simple, shallow data containers
=== Identical object handle No Identity checks, duplicate removal
Custom equals() method Freely defined properties As needed Value objects with functional equality
spl_object_id() Unique instance ID No Objects as a map key substitute
SplObjectStorage Object identity as a real key No Object to value mappings

Mironsoft

PHP modernization, code quality, and legacy refactoring

Grown PHP code nobody wants to touch anymore?

We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.

Legacy Refactoring

Modernize grown PHP code in a structured, low-risk way.

Establishing Code Quality

Anchor PHPStan, coding standards, and CI checks sustainably in the team.

Version Upgrades

Plan and execute PHP major version upgrades safely, without downtime.

10. Summary

Object Comparison == vs. ===

== on objects

Compares class and all properties recursively, no identity check.

=== on objects

Checks only whether both refer to the exact same instance in memory.

No __equals

PHP offers no magic method to customize comparison behavior.

Custom logic

equals() methods make functional equality explicit and testable.

11. FAQ: Object Comparison == vs. ===

1What exactly does == compare for two PHP objects?
Whether both objects are instances of the same class and all their properties, compared recursively, match.
2What does === check for objects?
Only whether both operands point to exactly the same internal object handle, meaning the same instance in memory.
3Does PHP have a magic method like __equals?
No, PHP offers no way to customize the behavior of == or === for a custom class.
4Why are two separately created objects equal with ==?
Because == only compares class and property values, not the memory address. With identical values == returns true, even for two independent instances.
5What happens with == on objects of different classes?
They are always unequal, even if both classes have identical properties with identical values.
6How should I check functional equality for value objects?
Best with an explicit equals() method that precisely defines which properties matter, instead of relying on ==
7What is spl_object_id useful for?
To produce a unique integer ID that stays stable for the object's lifetime, for example when objects need to serve as keys in a data structure.
8Can spl_object_id values be reused?
Yes, once the garbage collector removes an object, the same ID can be assigned to a later, new object.
9What is the advantage of SplObjectStorage over spl_object_id?
SplObjectStorage keeps a real reference to the object as the key, which eliminates the ID reuse problem entirely.
10Can the recursive == comparison become a problem for objects?
Yes, with deeply nested structures or circular references the comparison can become expensive or even end up in an infinite loop.