DEV Community

Cover image for What happens when we clone?
Doeke Norg
Doeke Norg

Posted on • Originally published at doeken.org

What happens when we clone?

It can sometimes be more useful to create a copy of an object and only change the relevant parts, then to completely recreate it. For example, when the object has a lot of dependencies or parameters you need to provide; while a second copy might only differ by one value.

To create such a copy you can use the clone keyword in PHP. When you make such a clone however, you are possibly not getting a full copy.

Shallow copies

When cloning a simple stdClass with a few scalar variables, a clone will be almost identical, but it will be a separate instance. So when we update a value on the second copy it will only be updated on that instance.

$object = (object) ['title' => 'Object title'];
$clone = clone $object;

var_dump($object == $clone); // true, they are equivalent
var_dump($object === $clone); // false, they not the same instance

$clone->title = 'Clone title';

var_dump($object->title === $clone->title); // false, the title are no longer identical
Enter fullscreen mode Exit fullscreen mode

However, when clone copies the object it will make what is known as a Shallow copy. This means it will keep all references that object has intact. So when you have an object or reference stored on a parameter, the instance of that reference will be the same for both copies.

$title = 'Object title';
$inner = (object) [];

$object = (object) [
    'title' => &$title, // The title parameter is now a reference to $title
    'inner' => $inner,
];
$clone = clone $object;
$clone->title = 'Clone title';

var_dump($object->title); // "Clone title"
var_dump($object->inner === $clone->inner); // true, the same instance
Enter fullscreen mode Exit fullscreen mode

This is important to keep in mind when working with clones. Because this could very well be annoying oversight or a very useful advantage.

Deep copies

If you need your nested objects to be a new instance as well, you need what is known as a Deep copy. This means that every reference will be a new instance of that reference as well.

To help with this you can use the magic __clone() method on the object you are cloning. This method is called on the object after the copy is made, but also before the object is returned. So you can use it to alter any parameters.

$object = new class {
    public \stdClass $inner;

    public function __construct()
    {
        $this->inner = (object) [];
    }

    public function __clone()
    {
        $this->inner = clone $this->inner;
    }
};

$clone = clone $object;

var_dump($object->inner == $clone->inner); // true, equivalent
var_dump($object->inner === $clone->inner); // false, not the same instance
Enter fullscreen mode Exit fullscreen mode

In this example we updated the $inner parameter with a new copy of itself. So now we are no longer referencing the same object. This means we now have a deep copy.

Note: Did you notice we referenced $this inside the __clone() method? The context bound to that method is not the current instance, but $this is actually referring to the clone!

A clone is not constructed

Another thing to be aware of is that the __construct() method is never called when an object is cloned. Just as with unserialize() the cloned object is recreated from the state of the original object; so there is no need to call the constructor.

$object = new class
{
    public string $title;

    public function __construct()
    {
        $this->title = 'Object title';
    }
};

$object->title = 'New title';
$clone = clone $object;

var_dump($clone->title); // "New title"
Enter fullscreen mode Exit fullscreen mode

As you can see, the new title on the clone is copied as well, while we might expect the cloned object to have Object title as its title.

Factories

Not calling the __construct() when cloning can be a powerful asset. It is used in a lot of factory classes to avoid possible heavy calls from being made. Imagine some heavy dependency that is created in the constructor. That same dependency can very likely be the same instance for every clone. This way the call is only made once, and then the instance is shared between every clone.

class User {
    public HeavyDependency $heavy_dependency;

    public function __construct() {
        $this->heavy_dependency = new HeavyDependency(/*...*/);
    }
}

class UserFactory {
    private User $prototype;

    public function create(): User
    {
        return clone $this->prototype ??= new User;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example we create a new instance of the User class on the first UserFactory::create() call. We then return a clone of that instance. Any subsequent calls to the create() method will already have the User instance, so we only return a clone. This cloning of a single instance to copy its dependencies is also known as the Prototype Pattern.

Set private variables on a clone without __clone()

One final cool trick is that you can actually create a clone and then set private variables on that clone, without the use of a __clone() method. To do this you must create a method on the object class that creates the clone and returns it. Inside this method you can still update private variables of the cloned instance.

$object = new class {
    private string $title = 'Object title';

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getClone(string $title): self
    {
        $clone = clone $this;
        $clone->title = $title;

        return $clone;
    }
};

$clone = $object->getClone('New title');

var_dump($clone->getTitle()); // "New title"
Enter fullscreen mode Exit fullscreen mode

Contrary to popular believe, a private variable is not private to the instance but to the class of that instance 🤯. So within the context of the same class, you can reference and update any private variable on such an instance.

As you can see this is useful for creating a clone, while also updating some of its values within the same call.

Thanks for reading

I hope you enjoyed reading this article! If so, please leave a ❤️ or a 🦄 and consider subscribing! I write posts on PHP almost every week. You can also follow me on twitter for more content and the occasional tip.

Top comments (0)