This content originally appeared on DEV Community and was authored by Doeke Norg
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
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
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
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"
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;
}
}
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"
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.
This content originally appeared on DEV Community and was authored by Doeke Norg
Doeke Norg | Sciencx (2021-09-24T16:54:54+00:00) What happens when we clone?. Retrieved from https://www.scien.cx/2021/09/24/what-happens-when-we-clone/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.