Don't clone your php objects, DeepCopy them

Disclaimer: The intent of this blog post is not for you to stop using the clone keyword but to raise awareness of its behavior since IMO in the documentation is not that clear.

As you know, PHP has a well-known clone keyword that shallow copy all of the object’s properties. So under the hood what it does is to create a new Object with the exact same values of that object properties – unless you change its behavior by implementing the __clone() function in your class.

This behavior seems what we expected. However, it might give “weird” results if the object that you are cloning contains properties that are objects. Let’s see:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php

final class Car {
    public $model; // making them public to write less code

    public function __construct(CarModel $model) {
        $this->model = $model;
    } 
}

final class CarModel {
    public $name;
    public $year;
    
    public function __construct($name, $year) {
        $this->name = $name;
        $this->year = $year;
    }
}

So these are two simple classes, one for my Car another for its Model. So now let’s see what happens when we create a new Car, clone it and change a model name of one of them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

$bmwX1 = new Car(new CarModel('X1', 2015));
$bmwX5 = clone $bmwX1;

var_dump(spl_object_hash($bmwX1)); // "0000000037e353af0000558c268309ea"
var_dump(spl_object_hash($bmwX5)); // "0000000037e353ac0000558c268309ea"

// So far all good, two objects with different ids.
// Let's see what happens to the model property in those objects

var_dump(spl_object_hash($bmwX1->model)); // "0000000037e353ad0000558c268309ea"
var_dump(spl_object_hash($bmwX5->model)); // "0000000037e353ad0000558c268309ea"

// As you can see the Model object in both objects has the same Id.
// This means if I change the model name in one of the objects it will be reflected in both.

$bmwX5->model->name = 'X5';

var_dump($bmwX1->model);

// object(CarModel)#2 (2) {
//   ["name"]=> "X5"
//   ["year"]=> int(2015)
// }

var_dump($bmwX5->model);

// object(CarModel)#2 (2) {
//   ["name"]=> "X5"
//   ["year"]=> int(2015)
// }

Would you expect this result? Probably not. So what’s happening here? PHP does not regenerate the memory address of objects that are properties in the object you are cloning nor traverses those properties to regenerate them. So how can we solve this? Fortunately, there is a Library for it! The DeepCopy lib is what we need. What DeepCopy does is recursively traverses all the object’s properties and clones them ensuring that every object inside the object you are cloning has a new instance of it hence it will have a new object Id. Let’s see a visual representation at how clone and DeepCopy works.

Clone DeepCopy

Let’s see how we can fix the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

use function DeepCopy\deep_copy;

$bmwX1 = new Car(new CarModel('X1', 2015));
$bmwX5 = deep_copy($bmwX1);

var_dump(spl_object_hash($bmwX1->model)); // "000000006042b54c000000001a8ebc46"
var_dump(spl_object_hash($bmwX5->model)); // "000000006042b543000000001a8ebc46"

// Now we have two different objects ids for the Model object in both objects bmwX1 and bmwX5.

$bmwX5->model->name = 'X5';

var_dump($bmwX1->model);

// object(CarModel)#2 (2) {
//   ["name"]=> string(2) "X1"
//   ["year"]=> int(2015)
// }

var_dump($bmwX5->model);

// object(CarModel)#13 (2) {
//   ["name"]=> string(2) "X5"
//   ["year"]=> int(2015)
// }

And it’s solved! I hope this helps you understand a bit better the clone keyword. Use it wisely.

edit 08-08-2018: Add disclaimer

comments powered by Disqus