objects namespaces and application architecture

Object Cloning and Comparison

Objects in PHP are assigned by reference to the same object handle. That means $b = $a does not create a second object. It creates another variable pointing at the same object.

This matters whenever objects are mutable. If two variables point at the same object, changing one changes what the other sees.

PHP example
<?php

declare(strict_types=1);

final class Basket
{
    /** @var list<string> */
    public array $items = [];
}

$first = new Basket();
$second = $first;

$second->items[] = 'Book';

echo count($first->items) . PHP_EOL;

// Prints:
// 1

There was only one Basket object. Both variables pointed to it.

Cloning Creates A New Object

The clone keyword creates a shallow copy of an object. The new object has the same property values, but it is a different object.

PHP example
<?php

declare(strict_types=1);

final class Basket
{
    /** @var list<string> */
    public array $items = [];
}

$first = new Basket();
$first->items[] = 'Book';

$second = clone $first;
$second->items[] = 'Pen';

echo count($first->items) . PHP_EOL;
echo count($second->items) . PHP_EOL;

// Prints:
// 1
// 2

The array property was copied, so changing the copied array did not change the original basket.

Cloning Is Shallow

Cloning is shallow when properties contain other objects. The outer object is copied, but nested objects are still shared unless you clone them yourself.

PHP example
<?php

declare(strict_types=1);

final class Customer
{
    public function __construct(
        public string $email,
    ) {
    }
}

final class Order
{
    public function __construct(
        public Customer $customer,
    ) {
    }
}

$original = new Order(new Customer('ada@example.com'));
$copy = clone $original;

$copy->customer->email = 'changed@example.com';

echo $original->customer->email . PHP_EOL;

// Prints:
// changed@example.com

The Order was cloned, but both orders still pointed at the same Customer.

Custom Cloning With __clone()

Use __clone() when a cloned object needs to copy nested objects or reset identity.

PHP example
<?php

declare(strict_types=1);

final class Customer
{
    public function __construct(
        public string $email,
    ) {
    }
}

final class Order
{
    public function __construct(
        public ?int $id,
        public Customer $customer,
    ) {
    }

    public function __clone(): void
    {
        $this->id = null;
        $this->customer = clone $this->customer;
    }
}

$original = new Order(100, new Customer('ada@example.com'));
$copy = clone $original;

$copy->customer->email = 'changed@example.com';

echo $original->id . ': ' . $original->customer->email . PHP_EOL;
echo ($copy->id ?? 'new') . ': ' . $copy->customer->email . PHP_EOL;

// Prints:
// 100: ada@example.com
// new: changed@example.com

Resetting the ID is common when cloning an entity to create a new unsaved record. Deep cloning nested objects prevents accidental shared mutation.

Comparing Objects With ===

=== checks whether two variables refer to the exact same object.

PHP example
<?php

declare(strict_types=1);

final class Product
{
    public function __construct(
        public int $id,
    ) {
    }
}

$a = new Product(10);
$b = $a;
$c = new Product(10);

echo $a === $b ? 'same object' : 'different object';
echo PHP_EOL;
echo $a === $c ? 'same object' : 'different object';
echo PHP_EOL;

// Prints:
// same object
// different object

Use === when identity matters.

Comparing Objects With ==

== compares object properties loosely. Two different objects of the same class can be equal with == if their properties match.

PHP example
<?php

declare(strict_types=1);

final class Product
{
    public function __construct(
        public int $id,
    ) {
    }
}

$a = new Product(10);
$b = new Product(10);

echo $a == $b ? 'equal values' : 'different values';
echo PHP_EOL;
echo $a === $b ? 'same object' : 'different object';
echo PHP_EOL;

// Prints:
// equal values
// different object

Be careful with == on objects. It can become surprising when objects have many properties, nested objects, or different classes.

Prefer Explicit Equality For Value Objects

For value objects, an explicit method such as equals() is often clearer than relying on ==.

PHP example
<?php

declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amountPence,
        public string $currency,
    ) {
    }

    public function equals(self $other): bool
    {
        return $this->amountPence === $other->amountPence
            && $this->currency === $other->currency;
    }
}

$first = new Money(1000, 'GBP');
$second = new Money(1000, 'GBP');

echo $first->equals($second) ? 'same value' : 'different value';
echo PHP_EOL;

// Prints:
// same value

The method states exactly what equality means for the concept.

What You Should Be Able To Do

After this lesson, you should be able to explain that assignment shares the same object, clone creates a shallow copy, __clone() customises cloning, === checks object identity, and == compares object properties.

For junior work, the practical skill is avoiding accidental shared mutation and choosing explicit equality checks when business meaning matters.

Practice

Practice: Clone An Order Safely

Create a small PHP example showing safe cloning and explicit comparison.

Task

Build:

  • a mutable Customer object
  • an Order object with an optional ID and a Customer
  • Order::__clone() that resets the ID and clones the customer
  • a value object with an equals() method

Use strict types. Keep the expected output in the PHP code block as printed lines or comments.

Check Your Work

Run cases for:

  • cloning an order and changing the copied customer email
  • proving the original customer email did not change
  • comparing two equal value objects with equals()

Afterward, explain why clone needed __clone() in this example.

Show solution

This solution customises cloning so the copied order becomes a new unsaved order and does not share the same mutable customer object.

PHP example
<?php

declare(strict_types=1);

final class Customer
{
    public function __construct(
        public string $email,
    ) {
    }
}

final class Order
{
    public function __construct(
        public ?int $id,
        public Customer $customer,
    ) {
    }

    public function __clone(): void
    {
        $this->id = null;
        $this->customer = clone $this->customer;
    }
}

final readonly class Money
{
    public function __construct(
        public int $amountPence,
        public string $currency,
    ) {
    }

    public function equals(self $other): bool
    {
        return $this->amountPence === $other->amountPence
            && $this->currency === $other->currency;
    }
}

$original = new Order(100, new Customer('ada@example.com'));
$copy = clone $original;
$copy->customer->email = 'copy@example.com';

echo $original->id . ': ' . $original->customer->email . PHP_EOL;
echo ($copy->id ?? 'new') . ': ' . $copy->customer->email . PHP_EOL;

$firstTotal = new Money(2500, 'GBP');
$secondTotal = new Money(2500, 'GBP');

echo $firstTotal->equals($secondTotal) ? 'same value' : 'different value';
echo PHP_EOL;

// Prints:
// 100: ada@example.com
// new: copy@example.com
// same value

__clone() is needed because normal cloning is shallow. Without it, the original and copied orders would share the same mutable Customer object.