Object Cloning, Comparison & Anonymous Classes
Understand shallow vs. deep clone semantics in depth, the precise difference between == and === for objects, when and how to use anonymous classes, and how dynamic method/property calls (variable method names) work in PHP.
Object mechanics in PHP cover the operations that sit slightly outside everyday method calls: making a copy of an object, deciding whether two objects are 'the same,' creating a throwaway class inline without naming it, and calling a method whose name is only known at runtime. Each of these looks like a small syntactic curiosity but reflects a real design decision PHP made about how objects behave — understanding the reasoning makes the syntax much easier to use correctly.
Clone semantics: shallow vs. deep, revisited
PHP objects are always handled by reference-like 'handles' internally: assigning $b = $a; for two objects does not copy the object — both variables point at the same underlying object, and mutating through either one is visible through the other. clone $a is PHP's explicit mechanism for producing a genuinely separate object, and by default it performs a shallow copy: every property is duplicated, but for properties that themselves hold objects, the duplicate property still references the exact same nested object as the source.
This means 'shallow vs. deep' isn't a global setting you choose once — it's decided property-by-property, inside __clone(), for whichever specific nested objects actually need independence. A ShoppingCart holding a CartItem[] array of objects, for example, needs its __clone() to loop over that array and clone each CartItem individually ($this->items = array_map(fn($item) => clone $item, $this->items);), because a shallow clone of an array of objects still leaves every element pointing at the original items.
Arrays themselves, notably, do NOT have this problem the same way objects do: PHP arrays have value semantics and are copied on assignment (technically copy-on-write, covered in the next lesson) — an array of scalars is always independent after clone or plain assignment. It is specifically arrays *of objects*, or object properties, where the shallow/deep distinction matters.
== vs === for objects
For objects, == (loose equality) checks that both operands are instances of the same class AND that all of their properties are equal (recursively, using == on each property pair) — it compares by value/structure, not identity. === (strict equality/identity) checks that both operands are literally the exact same object instance in memory — the same handle, not merely two separate objects that happen to have identical property values.
This distinction trips up developers coming from languages where == means identity by default. In PHP, new Point(1, 2) == new Point(1, 2) is true (same class, same property values) even though they are two entirely separate objects, while new Point(1, 2) === new Point(1, 2) is false (different instances). Meanwhile $a = new Point(1, 2); $b = $a; $a === $b; is true, because $b is not a new object — it's the same handle as $a.
The practical rule: use === when you specifically need to know 'is this literally the same object instance' (common for cache/registry lookups, or checking $user === $currentUser for an authorization check where you must be certain it's the exact same loaded object, not a look-alike). Use ==, or better, an explicit equals() method you write yourself, when you mean 'do these represent the same value' — value objects (Money, DateRange, Point) are usually better served by a hand-written equals() method than by relying on =='s recursive property comparison, because =='s behavior is implicit and can silently break if a class later gains a property that shouldn't be part of equality (like an internal cache field).
Anonymous classes
PHP 7+ supports anonymous classes: new class(...) { ... } defines and instantiates a class in one expression, with no name of its own (PHP generates an internal one you'd never reference directly). They support everything a normal class does — constructors, methods, properties, extends, implements, and even use for traits — just without a reusable, referenceable name.
The primary legitimate use case is a small, throwaway implementation of an interface or abstract class needed in exactly one place — most commonly in tests, where you need a quick stub/fake implementing some interface ($logger = new class implements LoggerInterface { public array $messages = []; public function log(string $msg): void { $this->messages[] = $msg; } };) without the ceremony of a separate named class file just for that one test. They're also occasionally used for simple event-listener or callback objects created inline where a full closure isn't expressive enough (e.g., needing multiple methods to satisfy a multi-method interface) but a whole named class feels like overkill for something used in one spot.
The trade-off is discoverability and reuse: an anonymous class cannot be type-hinted by name elsewhere, cannot be instanceof-checked against a name only it has (though instanceof SomeInterface still works fine if it implements a named interface), and if the same shape is needed in two places, you end up duplicating the definition rather than reusing a class. If you find yourself copy-pasting the same anonymous class expression more than once, that's the signal to extract it into a real, named class.
Dynamic method and property calls
PHP lets you call a method or access a property using a variable holding its name instead of a hardcoded identifier: $methodName = 'save'; $obj->$methodName(); calls $obj->save(), and $propName = 'email'; echo $obj->$propName; reads $obj->email. This works because PHP resolves ->$variable at runtime by looking up whatever string the variable currently holds — the syntax is genuinely dynamic, not just a shorthand.
The same applies to static calls and class names: $class = 'App\\Services\\Mailer'; $instance = new $class(); and $class::$staticMethod() both work, which is the low-level mechanism that plain string-based routing tables and simple factory patterns rely on (map a string key to a fully-qualified class name, then instantiate dynamically) — though production code more often reaches for reflection or a proper DI container for this, since dynamic instantiation from raw strings bypasses type-checking and autocomplete entirely.
This power comes with real risk: calling $obj->$methodName() where $methodName derives from unsanitized user input is a genuine security hazard, potentially letting an attacker invoke arbitrary methods on an object (a class of bug sometimes called 'method injection' or related to PHP object injection vulnerabilities). Never build a dynamic method/class name directly from raw request input without validating it against an explicit allow-list first.
Clone, comparison, anonymous class, and dynamic call syntax
== recursively compares class identity + all property values; === checks that both sides are literally the same object instance. Never build a dynamic method/class name string directly from raw, unvalidated user input — validate against an explicit allow-list first.
// Cloning
$copy = clone $original; // shallow copy; __clone() hook runs after
// Comparison
$a == $b; // same class + all properties loosely equal (value comparison)
$a === $b; // the exact same object instance (identity comparison)
// Anonymous class
$obj = new class(1, 2) implements SomeInterface {
public function __construct(public int $x, public int $y) {}
public function sum(): int { return $this->x + $this->y; }
};
// Dynamic method/property/class access
$methodName = 'save';
$obj->$methodName(); // same as $obj->save()
$propName = 'email';
$value = $obj->$propName; // same as $obj->email
$className = SomeClass::class;
$instance = new $className(); // dynamic instantiation from a stringCode Examples
== compares Point objects structurally (same class, equal property values), so two separately constructed Points with identical coordinates are == equal. === only returns true for $a and $c because $c was assigned directly from $a and is literally the same underlying object. Expected result: bool(true) bool(false) bool(true)
__clone() deep-clones the items array by mapping clone over each CartItem, so $cart and $checkoutCopy hold fully separate CartItem objects. Note the second output is 2, not 99: items() returns the private $items array by value (arrays are copy-on-write), so ->items()[0]->qty = 99 mutates a temporary copy of the array's object reference chain in a way that doesn't persist back into the private property — a good illustration of why array getters that return objects need care; a real mutation would go through a dedicated method like updateQty(). Expected result: 2 2
Common mistake: Expecting $checkoutCopy->items()[0]->qty = 99 to persist is a common misunderstanding — items() returns a value-copied array, so indexing into its return value for a mutation does not write back to the object's real internal state.
The anonymous class gives a minimal, self-contained Logger implementation inline, ideal for a one-off test spy. ActionDispatcher shows safe dynamic dispatch: the incoming action string is checked against an explicit allow-list constant before ever being used to build a method name, preventing arbitrary method invocation from untrusted input.
if (!in_array($action, self::ALLOWED, true)) { throw new \InvalidArgumentException(...); }
final class Point
{
public function __construct(public float $x, public float $y) {}
}
$a = new Point(1.0, 2.0);
$b = new Point(1.0, 2.0);
$c = $a;
var_dump($a == $b); // true - same class, equal properties
var_dump($a === $b); // false - two distinct object instances
var_dump($a === $c); // true - $c is the same instance as $afinal class CartItem
{
public function __construct(public string $sku, public int $qty) {}
}
final class ShoppingCart
{
/** @var CartItem[] */
private array $items = [];
public function add(CartItem $item): void { $this->items[] = $item; }
public function __clone(): void
{
// Deep-clone each CartItem so the copy's items are fully independent
$this->items = array_map(fn (CartItem $i) => clone $i, $this->items);
}
/** @return CartItem[] */
public function items(): array { return $this->items; }
}
$cart = new ShoppingCart();
$cart->add(new CartItem('SKU-1', 2));
$checkoutCopy = clone $cart;
$checkoutCopy->items()[0]->qty = 99; // mutate the COPY's array-returned item... but see below
echo $cart->items()[0]->qty . "\n"; // 2 (original untouched)
echo $checkoutCopy->items()[0]->qty . "\n"; // 2 (array access on a getter's returned copy of the array itself doesn't persist -- see explain)interface Logger { public function log(string $message): void; }
function runWithLogger(Logger $logger): void
{
$logger->log('starting');
$logger->log('done');
}
// Anonymous class: an inline test double implementing Logger,
// with no need for a separate named class file just for this test.
$spy = new class implements Logger {
public array $messages = [];
public function log(string $message): void { $this->messages[] = $message; }
};
runWithLogger($spy);
print_r($spy->messages);
// Dynamic dispatch: building a tiny action-name-to-method mapper safely,
// validated against an explicit allow-list (never trust raw input directly).
final class ActionDispatcher
{
private const ALLOWED = ['create', 'update', 'delete'];
public function dispatch(object $handler, string $action, mixed ...$args): mixed
{
if (!in_array($action, self::ALLOWED, true)) {
throw new \InvalidArgumentException("Unknown action: {$action}");
}
$method = $action . 'Item'; // e.g. 'create' -> 'createItem'
return $handler->$method(...$args);
}
}== structural equality vs === identity
== compares objects by structure/value; === compares them by identity (are they literally the same underlying object).
$a = new Point(1,2) $b = new Point(1,2) $c = $a
handle #101 handle #102 handle #101 (same as $a)
| | |
v v v
Point{x:1,y:2} Point{x:1,y:2} (points at #101 too)
$a == $b -> true (same class, equal property values)
$a === $b -> false (different handles: #101 vs #102)
$a === $c -> true (same handle: #101 == #101)Important Rules
clone performs a shallow copy; deep-clone specific nested object properties inside __clone() only where independence is actually needed.
== compares objects structurally (same class + all properties loosely equal); === compares by identity (the exact same object instance).
Anonymous classes (new class {...}) are ideal for one-off implementations (especially test doubles); extract to a named class the moment the same shape is needed in more than one place.
Dynamic property/method access ($obj->$var, $obj->$var()) resolves the name at runtime from whatever string the variable holds — never build such a name directly from unvalidated user input.
Arrays have value/copy-on-write semantics and don't suffer the shallow-clone-sharing problem the way object properties do — it's specifically nested objects (or arrays containing objects) that need explicit deep-cloning.
Where this shows up
PHPUnit and other testing tools use anonymous classes constantly for inline stubs and fakes implementing an interface under test, avoiding a proliferation of tiny named fixture classes. Value-object libraries (money, UUID, date-range packages) universally implement explicit equals() methods rather than relying on ==, precisely because internal-only properties (caches, lazily-computed fields) would otherwise silently break equality checks. Simple factory/service-locator patterns use dynamic instantiation (new $className()) behind a validated allow-list or a DI container, rather than trusting raw strings directly — this is also the exact vulnerability class (PHP Object Injection) that security audits check for whenever user input reaches unserialize() or a dynamically-built class/method name.
// Relying on == for value objects that might later gain unrelated properties class Money { public int $cents; public ?string $debugLabel = null; } $a = new Money(); $a->cents = 500; $b = new Money(); $b->cents = 500; $b->debugLabel = 'test-only'; $a == $b; // false! debugLabel differs, even though the 'real' value (cents) is equal class Money { public function __construct(public readonly int $cents) {} public function equals(Money $other): bool { return $this->cents === $other->cents; } } $a->equals($b); // compares exactly the fields that define equality, explicitly
== compares every property, including ones that were never meant to be part of 'equality' in a domain sense (like a debug-only label). A hand-written equals() method makes exactly which fields matter for equality explicit and immune to being silently broken by adding an unrelated property later.
[multiple-choice] For two objects $a and $b of the same class with identical property values but created by two separate new calls, what do == and === return respectively? A. true, true B. false, false C. true, false D. false, true
C. true, false — == compares structurally (same class, equal properties), so it returns true. === compares identity (are they the same underlying object instance), and since they came from two separate new calls, it returns false.
[multiple-choice] What is the most appropriate use case for an anonymous class? A. Defining your application's primary domain models B. A one-off implementation of an interface, most commonly a test stub/fake needed in exactly one place C. Replacing all named classes in a codebase to reduce file count D. Improving performance compared to a named class
B. A one-off implementation of an interface, most commonly a test stub/fake needed in exactly one place — Anonymous classes are best for small, throwaway implementations needed in exactly one place — classic test doubles are the most common real use. Once the same shape is needed elsewhere, extracting a named class is better for reuse and discoverability.
[predict-output] Given $name = 'log'; class Foo { public function log() { return 'logged'; } } $foo = new Foo(); echo $foo->$name(); what is printed? A. An error: $name is not a valid method reference B. logged C. log D. Nothing is printed
B. logged — $foo->$name() resolves $name to the string 'log' at runtime and calls $foo->log(), which returns 'logged' — PHP's dynamic method call syntax evaluates the variable to get the actual method name to invoke.
Write a class Playlist holding a private array of Song objects (each Song has a public string $title). Implement __clone() so cloning a Playlist produces fully independent Song objects. Then write an anonymous class implementing a Comparator interface (with a method compare(Song $a, Song $b): int) that sorts songs alphabetically by title, and use it with usort().
interface Comparator { public function compare(Song $a, Song $b): int; } final class Song { public function __construct(public string $title) {} } final class Playlist { /** @var Song[] */ private array $songs = []; public function add(Song $s): void { $this->songs[] = $s; } public function __clone(): void { $this->songs = array_map(fn (Song $s) => clone $s, $this->songs); } public function sortWith(Comparator $cmp): void { usort($this->songs, fn ($a, $b) => $cmp->compare($a, $b)); } } $alphabetical = new class implements Comparator { public function compare(Song $a, Song $b): int { return strcmp($a->title, $b->title); } };
For __clone(), map clone over the private songs array, same as the ShoppingCart example. For the anonymous class, implement compare() using strcmp($a->title, $b->title).